diff --git a/README.md b/README.md
index 16efea5..37a4403 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# EnakloPOS
+# ApskelPOS
A new Flutter project.
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 7ecc902..3eb4ebd 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -23,7 +23,7 @@ if (flutterVersionName == null) {
}
android {
- namespace "com.example.enaklo_pos"
+ namespace "com.appscale.pos"
compileSdkVersion 35
ndkVersion flutter.ndkVersion
@@ -42,7 +42,7 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
- applicationId "com.example.enaklo_pos"
+ applicationId "com.appscale.pos"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion 21
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 0b2dde7..159867a 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -11,7 +11,7 @@
-
+
+
+
diff --git a/android/app/src/main/res/mipmap-hdpi/launcher_icon.png b/android/app/src/main/res/mipmap-hdpi/launcher_icon.png
index 255f6e7..8a735f2 100644
Binary files a/android/app/src/main/res/mipmap-hdpi/launcher_icon.png and b/android/app/src/main/res/mipmap-hdpi/launcher_icon.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/launcher_icon.png b/android/app/src/main/res/mipmap-mdpi/launcher_icon.png
index bf1325e..f52a995 100644
Binary files a/android/app/src/main/res/mipmap-mdpi/launcher_icon.png and b/android/app/src/main/res/mipmap-mdpi/launcher_icon.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png
index b76448e..6fc97e6 100644
Binary files a/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png and b/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png
index 6e71dd1..9fc4359 100644
Binary files a/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png and b/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png
index bbe6bc1..43a4915 100644
Binary files a/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png and b/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png differ
diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml
index c5d5899..ab98328 100644
--- a/android/app/src/main/res/values/colors.xml
+++ b/android/app/src/main/res/values/colors.xml
@@ -1,4 +1,4 @@
- #FFFFFF
+ #ffffff
\ No newline at end of file
diff --git a/assets/fonts/quicksand/Quicksand-Bold.ttf b/assets/fonts/quicksand/Quicksand-Bold.ttf
new file mode 100644
index 0000000..0106805
Binary files /dev/null and b/assets/fonts/quicksand/Quicksand-Bold.ttf differ
diff --git a/assets/fonts/quicksand/Quicksand-Light.ttf b/assets/fonts/quicksand/Quicksand-Light.ttf
new file mode 100644
index 0000000..fcf86af
Binary files /dev/null and b/assets/fonts/quicksand/Quicksand-Light.ttf differ
diff --git a/assets/fonts/quicksand/Quicksand-Medium.ttf b/assets/fonts/quicksand/Quicksand-Medium.ttf
new file mode 100644
index 0000000..afca2d9
Binary files /dev/null and b/assets/fonts/quicksand/Quicksand-Medium.ttf differ
diff --git a/assets/fonts/quicksand/Quicksand-Regular.ttf b/assets/fonts/quicksand/Quicksand-Regular.ttf
new file mode 100644
index 0000000..c03548a
Binary files /dev/null and b/assets/fonts/quicksand/Quicksand-Regular.ttf differ
diff --git a/assets/fonts/quicksand/Quicksand-SemiBold.ttf b/assets/fonts/quicksand/Quicksand-SemiBold.ttf
new file mode 100644
index 0000000..83475ea
Binary files /dev/null and b/assets/fonts/quicksand/Quicksand-SemiBold.ttf differ
diff --git a/assets/icons/people.svg b/assets/icons/people.svg
new file mode 100644
index 0000000..f956bf1
--- /dev/null
+++ b/assets/icons/people.svg
@@ -0,0 +1,10 @@
+
diff --git a/assets/images/gojek.png b/assets/images/gojek.png
new file mode 100644
index 0000000..75548ee
Binary files /dev/null and b/assets/images/gojek.png differ
diff --git a/assets/images/grab.png b/assets/images/grab.png
new file mode 100644
index 0000000..09ffcbd
Binary files /dev/null and b/assets/images/grab.png differ
diff --git a/assets/logo/logo.png b/assets/logo/logo.png
index 733fa8c..93f2c08 100644
Binary files a/assets/logo/logo.png and b/assets/logo/logo.png differ
diff --git a/generate_app_icon.dart b/generate_app_icon.dart
index da080e4..4fd8bd9 100644
--- a/generate_app_icon.dart
+++ b/generate_app_icon.dart
@@ -4,31 +4,30 @@ import 'lib/core/utils/app_icon_generator.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
-
- print('Generating EnakloPOS app icon...');
-
+
+ print('Generating ApskelPOS app icon...');
+
try {
final iconData = await AppIconGenerator.generateAppIcon();
-
+
// Ensure the assets/logo directory exists
final logoDir = Directory('assets/logo');
if (!await logoDir.exists()) {
await logoDir.create(recursive: true);
}
-
+
// Write the generated icon to file
- final iconFile = File('assets/logo/logo_app_icon.png');
+ final iconFile = File('assets/logo/ic_launcher.png');
await iconFile.writeAsBytes(iconData);
-
- print('✅ App icon generated successfully at: assets/logo/logo_app_icon.png');
+
+ print('✅ App icon generated successfully at: assets/logo/ic_launcher.png');
print('📱 The icon features:');
print(' - White background for visibility');
print(' - Blue circular background');
print(' - Gift box with "e" inside');
print(' - "ENAKLO" and "POS" text');
print(' - 1024x1024 resolution for high quality');
-
} catch (e) {
print('❌ Error generating app icon: $e');
}
-}
\ No newline at end of file
+}
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
index d36b1fa..d0d98aa 100644
--- a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -1,122 +1 @@
-{
- "images" : [
- {
- "size" : "20x20",
- "idiom" : "iphone",
- "filename" : "Icon-App-20x20@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "20x20",
- "idiom" : "iphone",
- "filename" : "Icon-App-20x20@3x.png",
- "scale" : "3x"
- },
- {
- "size" : "29x29",
- "idiom" : "iphone",
- "filename" : "Icon-App-29x29@1x.png",
- "scale" : "1x"
- },
- {
- "size" : "29x29",
- "idiom" : "iphone",
- "filename" : "Icon-App-29x29@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "29x29",
- "idiom" : "iphone",
- "filename" : "Icon-App-29x29@3x.png",
- "scale" : "3x"
- },
- {
- "size" : "40x40",
- "idiom" : "iphone",
- "filename" : "Icon-App-40x40@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "40x40",
- "idiom" : "iphone",
- "filename" : "Icon-App-40x40@3x.png",
- "scale" : "3x"
- },
- {
- "size" : "60x60",
- "idiom" : "iphone",
- "filename" : "Icon-App-60x60@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "60x60",
- "idiom" : "iphone",
- "filename" : "Icon-App-60x60@3x.png",
- "scale" : "3x"
- },
- {
- "size" : "20x20",
- "idiom" : "ipad",
- "filename" : "Icon-App-20x20@1x.png",
- "scale" : "1x"
- },
- {
- "size" : "20x20",
- "idiom" : "ipad",
- "filename" : "Icon-App-20x20@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "29x29",
- "idiom" : "ipad",
- "filename" : "Icon-App-29x29@1x.png",
- "scale" : "1x"
- },
- {
- "size" : "29x29",
- "idiom" : "ipad",
- "filename" : "Icon-App-29x29@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "40x40",
- "idiom" : "ipad",
- "filename" : "Icon-App-40x40@1x.png",
- "scale" : "1x"
- },
- {
- "size" : "40x40",
- "idiom" : "ipad",
- "filename" : "Icon-App-40x40@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "76x76",
- "idiom" : "ipad",
- "filename" : "Icon-App-76x76@1x.png",
- "scale" : "1x"
- },
- {
- "size" : "76x76",
- "idiom" : "ipad",
- "filename" : "Icon-App-76x76@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "83.5x83.5",
- "idiom" : "ipad",
- "filename" : "Icon-App-83.5x83.5@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "1024x1024",
- "idiom" : "ios-marketing",
- "filename" : "Icon-App-1024x1024@1x.png",
- "scale" : "1x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
+{"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}}
\ No newline at end of file
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
index fe17dc9..1c10fac 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
index 62e6e0a..f1bc452 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
index 4fd1115..1a6a719 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
index bb37dbb..ea51316 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
index 66b7659..030b15e 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
index 697aa91..95717a7 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
index a3d490b..a131b69 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
index 4fd1115..1a6a719 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
index 881007f..b056c30 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
index 8fd7e63..2914128 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png
index f7d3438..306f89c 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png
index dea8518..9e23f21 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png
index 5d60416..7b14acd 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png
index 4c51fa9..639dee4 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
index 8fd7e63..2914128 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
index aec4644..b339a99 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png
index 2105988..a3020db 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png
index baf1a6a..39567bf 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
index 3c68433..4bdd792 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
index adc2635..f179387 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
index f2bfd8b..c5d5e2b 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 0a7712c..4353496 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -5,7 +5,7 @@
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleDisplayName
- EnakloPOS
+ ApskelPOS
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
diff --git a/launcher_icon.yaml b/launcher_icon.yaml
new file mode 100644
index 0000000..002d378
--- /dev/null
+++ b/launcher_icon.yaml
@@ -0,0 +1,17 @@
+# Generate: dart run flutter_launcher_icons -f launcher_icon.yaml
+
+flutter_launcher_icons:
+ android: "launcher_icon"
+ ios: true
+ image_path: "assets/logo/logo.png"
+ remove_alpha_ios: true
+ min_sdk_android: 21 # android min sdk min:16, default 21
+ adaptive_icon_background: "#ffffff"
+ adaptive_icon_foreground: "assets/logo/logo.png"
+ web:
+ generate: true
+ image_path: "assets/logo/logo.png"
+ windows:
+ generate: true
+ image_path: "assets/logo/logo.png"
+ icon_size: 48
diff --git a/lib/core/assets/assets.gen.dart b/lib/core/assets/assets.gen.dart
index 85b1d40..ae39032 100644
--- a/lib/core/assets/assets.gen.dart
+++ b/lib/core/assets/assets.gen.dart
@@ -101,6 +101,9 @@ class $AssetsIconsGen {
/// File path: assets/icons/payments.svg
SvgGenImage get payments => const SvgGenImage('assets/icons/payments.svg');
+ /// File path: assets/icons/people.svg
+ SvgGenImage get people => const SvgGenImage('assets/icons/people.svg');
+
/// File path: assets/icons/print.svg
SvgGenImage get print => const SvgGenImage('assets/icons/print.svg');
@@ -152,6 +155,7 @@ class $AssetsIconsGen {
orders,
pajak,
payments,
+ people,
print,
qrCode,
report,
@@ -186,6 +190,12 @@ class $AssetsImagesGen {
/// File path: assets/images/drink7.png
AssetGenImage get drink7 => const AssetGenImage('assets/images/drink7.png');
+ /// File path: assets/images/gojek.png
+ AssetGenImage get gojek => const AssetGenImage('assets/images/gojek.png');
+
+ /// File path: assets/images/grab.png
+ AssetGenImage get grab => const AssetGenImage('assets/images/grab.png');
+
/// File path: assets/images/logo.png
AssetGenImage get logo => const AssetGenImage('assets/images/logo.png');
@@ -265,6 +275,8 @@ class $AssetsImagesGen {
drink5,
drink6,
drink7,
+ gojek,
+ grab,
logo,
managePrinter,
manageProduct,
diff --git a/lib/core/assets/fonts.gen.dart b/lib/core/assets/fonts.gen.dart
new file mode 100644
index 0000000..e6d6e31
--- /dev/null
+++ b/lib/core/assets/fonts.gen.dart
@@ -0,0 +1,15 @@
+/// GENERATED CODE - DO NOT MODIFY BY HAND
+/// *****************************************************
+/// FlutterGen
+/// *****************************************************
+
+// coverage:ignore-file
+// ignore_for_file: type=lint
+// ignore_for_file: directives_ordering,unnecessary_import,implicit_dynamic_list_literal,deprecated_member_use
+
+class FontFamily {
+ FontFamily._();
+
+ /// Font family: Quicksand
+ static const String quicksand = 'Quicksand';
+}
diff --git a/lib/core/components/buttons.dart b/lib/core/components/buttons.dart
index 2301468..bcdc058 100644
--- a/lib/core/components/buttons.dart
+++ b/lib/core/components/buttons.dart
@@ -18,6 +18,10 @@ class Button extends StatelessWidget {
this.icon,
this.disabled = false,
this.fontSize = 16.0,
+ this.elevation,
+ this.labelStyle,
+ this.mainAxisAlignment = MainAxisAlignment.center,
+ this.crossAxisAlignment = CrossAxisAlignment.center,
});
const Button.outlined({
@@ -33,9 +37,13 @@ class Button extends StatelessWidget {
this.icon,
this.disabled = false,
this.fontSize = 16.0,
+ this.elevation,
+ this.labelStyle,
+ this.mainAxisAlignment = MainAxisAlignment.center,
+ this.crossAxisAlignment = CrossAxisAlignment.center,
});
- final Function() onPressed;
+ final Function()? onPressed;
final String label;
final ButtonStyle style;
final Color color;
@@ -43,9 +51,13 @@ class Button extends StatelessWidget {
final double? width;
final double height;
final double borderRadius;
+ final double? elevation;
final Widget? icon;
final bool disabled;
final double fontSize;
+ final TextStyle? labelStyle;
+ final MainAxisAlignment mainAxisAlignment;
+ final CrossAxisAlignment crossAxisAlignment;
@override
Widget build(BuildContext context) {
@@ -60,11 +72,12 @@ class Button extends StatelessWidget {
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(borderRadius),
),
+ elevation: elevation,
padding: const EdgeInsets.symmetric(horizontal: 16.0),
),
child: Row(
- mainAxisAlignment: MainAxisAlignment.center,
- mainAxisSize: MainAxisSize.min,
+ mainAxisAlignment: mainAxisAlignment,
+ crossAxisAlignment: crossAxisAlignment,
children: [
icon ?? const SizedBox.shrink(),
if (icon != null) const SizedBox(width: 10.0),
@@ -73,11 +86,12 @@ class Button extends StatelessWidget {
fit: BoxFit.scaleDown,
child: Text(
label,
- style: TextStyle(
- color: disabled ? Colors.grey : textColor,
- fontSize: fontSize,
- fontWeight: FontWeight.w600,
- ),
+ style: labelStyle ??
+ TextStyle(
+ color: disabled ? Colors.grey : textColor,
+ fontSize: fontSize,
+ fontWeight: FontWeight.bold,
+ ),
textAlign: TextAlign.center,
),
),
@@ -89,14 +103,15 @@ class Button extends StatelessWidget {
onPressed: disabled ? null : onPressed,
style: OutlinedButton.styleFrom(
backgroundColor: color,
- side: const BorderSide(color: Colors.grey),
+ side: const BorderSide(color: AppColors.primary),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(borderRadius),
),
padding: const EdgeInsets.symmetric(horizontal: 16.0),
),
child: Row(
- mainAxisAlignment: MainAxisAlignment.center,
+ mainAxisAlignment: mainAxisAlignment,
+ crossAxisAlignment: crossAxisAlignment,
mainAxisSize: MainAxisSize.min,
children: [
icon ?? const SizedBox.shrink(),
@@ -106,11 +121,12 @@ class Button extends StatelessWidget {
fit: BoxFit.scaleDown,
child: Text(
label,
- style: TextStyle(
- color: disabled ? Colors.grey : textColor,
- fontSize: fontSize,
- fontWeight: FontWeight.w600,
- ),
+ style: labelStyle ??
+ TextStyle(
+ color: disabled ? Colors.grey : textColor,
+ fontSize: fontSize,
+ fontWeight: FontWeight.w600,
+ ),
textAlign: TextAlign.center,
),
),
diff --git a/lib/core/components/custom_modal_dialog.dart b/lib/core/components/custom_modal_dialog.dart
new file mode 100644
index 0000000..8d943be
--- /dev/null
+++ b/lib/core/components/custom_modal_dialog.dart
@@ -0,0 +1,117 @@
+import 'package:enaklo_pos/core/components/spaces.dart';
+import 'package:enaklo_pos/core/constants/colors.dart';
+import 'package:enaklo_pos/core/extensions/build_context_ext.dart';
+import 'package:flutter/material.dart';
+
+class CustomModalDialog extends StatelessWidget {
+ final String title;
+ final String? subtitle;
+ final Widget child;
+ final VoidCallback? onClose;
+ final double? minWidth;
+ final double? maxWidth;
+ final double? minHeight;
+ final double? maxHeight;
+ final EdgeInsets? contentPadding;
+
+ const CustomModalDialog({
+ super.key,
+ required this.title,
+ this.subtitle,
+ required this.child,
+ this.onClose,
+ this.minWidth,
+ this.maxWidth,
+ this.minHeight,
+ this.maxHeight,
+ this.contentPadding,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Dialog(
+ backgroundColor: AppColors.white,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(16),
+ ),
+ child: ConstrainedBox(
+ constraints: BoxConstraints(
+ minWidth: minWidth ?? context.deviceWidth * 0.3,
+ maxWidth: maxWidth ?? context.deviceWidth * 0.8,
+ minHeight: minHeight ?? context.deviceHeight * 0.3,
+ maxHeight: maxHeight ?? context.deviceHeight * 0.8,
+ ),
+ child: IntrinsicWidth(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Container(
+ padding: const EdgeInsets.all(16),
+ width: double.infinity,
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ colors: [
+ const Color.fromARGB(255, 81, 40, 134),
+ AppColors.primary,
+ ],
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ ),
+ borderRadius: BorderRadius.vertical(
+ top: Radius.circular(16),
+ ),
+ ),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ title,
+ style: TextStyle(
+ color: AppColors.white,
+ fontSize: 20,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ if (subtitle != null)
+ Text(
+ subtitle ?? '',
+ style: TextStyle(
+ color: AppColors.grey,
+ fontSize: 16,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ ],
+ ),
+ ),
+ SpaceWidth(12),
+ IconButton(
+ icon: Icon(Icons.close, color: AppColors.white),
+ onPressed: () {
+ if (onClose != null) {
+ onClose!();
+ } else {
+ Navigator.of(context).pop();
+ }
+ },
+ ),
+ ],
+ ),
+ ),
+ Flexible(
+ child: SingleChildScrollView(
+ padding: contentPadding ?? EdgeInsets.zero,
+ child: child,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/core/components/custom_text_field.dart b/lib/core/components/custom_text_field.dart
index cbc6828..3e677fc 100644
--- a/lib/core/components/custom_text_field.dart
+++ b/lib/core/components/custom_text_field.dart
@@ -14,6 +14,8 @@ class CustomTextField extends StatelessWidget {
final Widget? prefixIcon;
final Widget? suffixIcon;
final bool readOnly;
+ final int maxLines;
+ final String? Function(String?)? validator;
const CustomTextField({
super.key,
@@ -28,6 +30,8 @@ class CustomTextField extends StatelessWidget {
this.prefixIcon,
this.suffixIcon,
this.readOnly = false,
+ this.maxLines = 1,
+ this.validator,
});
@override
@@ -53,17 +57,11 @@ class CustomTextField extends StatelessWidget {
textInputAction: textInputAction,
textCapitalization: textCapitalization ?? TextCapitalization.none,
readOnly: readOnly,
+ maxLines: maxLines,
+ validator: validator,
decoration: InputDecoration(
prefixIcon: prefixIcon,
suffixIcon: suffixIcon,
- border: OutlineInputBorder(
- borderRadius: BorderRadius.circular(16.0),
- borderSide: const BorderSide(color: Colors.grey),
- ),
- enabledBorder: OutlineInputBorder(
- borderRadius: BorderRadius.circular(16.0),
- borderSide: const BorderSide(color: Colors.grey),
- ),
hintText: label,
),
),
diff --git a/lib/core/components/dashed_divider.dart b/lib/core/components/dashed_divider.dart
new file mode 100644
index 0000000..5cb3fad
--- /dev/null
+++ b/lib/core/components/dashed_divider.dart
@@ -0,0 +1,41 @@
+import 'package:flutter/material.dart';
+
+class DashedDivider extends StatelessWidget {
+ final double height;
+ final double dashWidth;
+ final double dashSpacing;
+ final Color color;
+
+ const DashedDivider({
+ super.key,
+ this.height = 1,
+ this.dashWidth = 5,
+ this.dashSpacing = 3,
+ this.color = Colors.grey,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return SizedBox(
+ height: height,
+ child: LayoutBuilder(
+ builder: (context, constraints) {
+ final boxWidth = constraints.constrainWidth();
+ final dashCount = (boxWidth / (dashWidth + dashSpacing)).floor();
+ return Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: List.generate(dashCount, (_) {
+ return SizedBox(
+ width: dashWidth,
+ height: height,
+ child: DecoratedBox(
+ decoration: BoxDecoration(color: color),
+ ),
+ );
+ }),
+ );
+ },
+ ),
+ );
+ }
+}
diff --git a/lib/core/components/flushbar.dart b/lib/core/components/flushbar.dart
new file mode 100644
index 0000000..3563f07
--- /dev/null
+++ b/lib/core/components/flushbar.dart
@@ -0,0 +1,48 @@
+import 'package:another_flushbar/flushbar.dart';
+import 'package:flutter/material.dart';
+
+class AppFlushbar {
+ static void showSuccess(BuildContext context, String message) {
+ Flushbar(
+ messageText: Text(
+ message,
+ style: const TextStyle(
+ color: Colors.white,
+ fontWeight: FontWeight.bold,
+ fontSize: 16,
+ ),
+ ),
+ icon: const Icon(
+ Icons.check_circle,
+ color: Colors.white,
+ ),
+ duration: const Duration(seconds: 2),
+ flushbarPosition: FlushbarPosition.BOTTOM,
+ backgroundColor: Colors.green,
+ borderRadius: BorderRadius.circular(12),
+ margin: const EdgeInsets.all(12),
+ ).show(context);
+ }
+
+ static void showError(BuildContext context, String message) {
+ Flushbar(
+ messageText: Text(
+ message,
+ style: const TextStyle(
+ color: Colors.white,
+ fontWeight: FontWeight.bold,
+ fontSize: 16,
+ ),
+ ),
+ icon: const Icon(
+ Icons.error,
+ color: Colors.white,
+ ),
+ duration: const Duration(seconds: 3),
+ flushbarPosition: FlushbarPosition.BOTTOM,
+ backgroundColor: Colors.red,
+ borderRadius: BorderRadius.circular(12),
+ margin: const EdgeInsets.all(12),
+ ).show(context);
+ }
+}
diff --git a/lib/core/components/image_picker_widget.dart b/lib/core/components/image_picker_widget.dart
index 010fa6e..cc29a03 100644
--- a/lib/core/components/image_picker_widget.dart
+++ b/lib/core/components/image_picker_widget.dart
@@ -1,91 +1,189 @@
import 'dart:io';
+import 'package:enaklo_pos/presentation/setting/bloc/upload_file/upload_file_bloc.dart';
import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:image_picker/image_picker.dart';
import 'package:cached_network_image/cached_network_image.dart';
-import '../assets/assets.gen.dart';
import '../constants/colors.dart';
import '../constants/variables.dart';
-import 'buttons.dart';
import 'spaces.dart';
class ImagePickerWidget extends StatefulWidget {
final String label;
final void Function(XFile? file) onChanged;
+ final void Function(String? uploadedUrl)? onUploaded;
final bool showLabel;
final String? initialImageUrl;
+ final bool autoUpload;
const ImagePickerWidget({
super.key,
required this.label,
required this.onChanged,
+ this.onUploaded,
this.showLabel = true,
this.initialImageUrl,
+ this.autoUpload = false,
});
@override
State createState() => _ImagePickerWidgetState();
}
-class _ImagePickerWidgetState extends State {
+class _ImagePickerWidgetState extends State
+ with TickerProviderStateMixin {
String? imagePath;
+ String? uploadedImageUrl;
bool hasInitialImage = false;
+ bool isHovering = false;
+ bool isUploading = false;
+ late AnimationController _scaleController;
+ late AnimationController _fadeController;
+ late AnimationController _uploadController;
+ late Animation _scaleAnimation;
+ late Animation _fadeAnimation;
+ late Animation _uploadAnimation;
@override
void initState() {
super.initState();
hasInitialImage = widget.initialImageUrl != null;
+
+ _scaleController = AnimationController(
+ duration: const Duration(milliseconds: 200),
+ vsync: this,
+ );
+ _fadeController = AnimationController(
+ duration: const Duration(milliseconds: 300),
+ vsync: this,
+ );
+ _uploadController = AnimationController(
+ duration: const Duration(milliseconds: 1000),
+ vsync: this,
+ );
+
+ _scaleAnimation = Tween(
+ begin: 1.0,
+ end: 0.95,
+ ).animate(CurvedAnimation(
+ parent: _scaleController,
+ curve: Curves.easeInOut,
+ ));
+
+ _fadeAnimation = Tween(
+ begin: 0.0,
+ end: 1.0,
+ ).animate(CurvedAnimation(
+ parent: _fadeController,
+ curve: Curves.easeInOut,
+ ));
+
+ _uploadAnimation = Tween(
+ begin: 0.0,
+ end: 1.0,
+ ).animate(CurvedAnimation(
+ parent: _uploadController,
+ curve: Curves.easeInOut,
+ ));
+
+ _fadeController.forward();
+ }
+
+ @override
+ void dispose() {
+ _scaleController.dispose();
+ _fadeController.dispose();
+ _uploadController.dispose();
+ super.dispose();
}
Future _pickImage() async {
+ _scaleController.forward().then((_) {
+ _scaleController.reverse();
+ });
+
final pickedFile = await ImagePicker().pickImage(
source: ImageSource.gallery,
);
- setState(() {
- if (pickedFile != null) {
+ if (pickedFile != null) {
+ setState(() {
imagePath = pickedFile.path;
- hasInitialImage = false; // Clear initial image when new image is picked
- widget.onChanged(pickedFile);
- } else {
- debugPrint('No image selected.');
- widget.onChanged(null);
+ hasInitialImage = false;
+ uploadedImageUrl = null;
+ });
+
+ widget.onChanged(pickedFile);
+
+ // Auto upload if enabled
+ if (widget.autoUpload) {
+ _uploadImage(pickedFile.path);
}
- });
+ } else {
+ debugPrint('No image selected.');
+ widget.onChanged(null);
+ }
}
- @override
- Widget build(BuildContext context) {
- return Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- if (widget.showLabel) ...[
- Text(
- widget.label,
- style: const TextStyle(
- fontSize: 14,
- fontWeight: FontWeight.w700,
- ),
+ void _uploadImage(String filePath) {
+ setState(() {
+ isUploading = true;
+ });
+ _uploadController.forward();
+
+ context.read().add(
+ UploadFileEvent.upload(filePath),
+ );
+ }
+
+ Widget _buildImageContainer() {
+ return Container(
+ width: 100.0,
+ height: 100.0,
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(20.0),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withOpacity(0.1),
+ blurRadius: 10,
+ offset: const Offset(0, 4),
),
- const SpaceHeight(12.0),
],
- Container(
- padding: const EdgeInsets.all(6.0),
- decoration: BoxDecoration(
- borderRadius: BorderRadius.circular(16.0),
- border: Border.all(color: AppColors.primary),
- ),
- child: Row(
- children: [
- SizedBox(
- width: 80.0,
- height: 80.0,
- child: ClipRRect(
- borderRadius: BorderRadius.circular(10.0),
- child: imagePath != null
- ? Image.file(
- File(imagePath!),
+ ),
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(20.0),
+ child: Stack(
+ children: [
+ Positioned.fill(
+ child: imagePath != null
+ ? Image.file(
+ File(imagePath!),
+ fit: BoxFit.cover,
+ )
+ : uploadedImageUrl != null
+ ? CachedNetworkImage(
+ imageUrl: uploadedImageUrl!.contains('http')
+ ? uploadedImageUrl!
+ : '${Variables.baseUrl}/$uploadedImageUrl',
+ placeholder: (context, url) => Container(
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ colors: [
+ AppColors.primary.withOpacity(0.1),
+ AppColors.primary.withOpacity(0.05),
+ ],
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ ),
+ ),
+ child: const Center(
+ child: CircularProgressIndicator(strokeWidth: 2),
+ ),
+ ),
+ errorWidget: (context, url, error) =>
+ _buildPlaceholder(),
fit: BoxFit.cover,
)
: hasInitialImage && widget.initialImageUrl != null
@@ -93,38 +191,493 @@ class _ImagePickerWidgetState extends State {
imageUrl: widget.initialImageUrl!.contains('http')
? widget.initialImageUrl!
: '${Variables.baseUrl}/${widget.initialImageUrl}',
- placeholder: (context, url) =>
- const Center(child: CircularProgressIndicator()),
- errorWidget: (context, url, error) => Container(
- padding: const EdgeInsets.all(16.0),
- color: AppColors.black.withOpacity(0.05),
- child: Assets.icons.image.svg(),
+ placeholder: (context, url) => Container(
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ colors: [
+ AppColors.primary.withOpacity(0.1),
+ AppColors.primary.withOpacity(0.05),
+ ],
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ ),
+ ),
+ child: const Center(
+ child:
+ CircularProgressIndicator(strokeWidth: 2),
+ ),
),
+ errorWidget: (context, url, error) =>
+ _buildPlaceholder(),
fit: BoxFit.cover,
)
- : Container(
- padding: const EdgeInsets.all(16.0),
- color: AppColors.black.withOpacity(0.05),
- child: Assets.icons.image.svg(),
- ),
+ : _buildPlaceholder(),
+ ),
+ // Upload progress overlay
+ if (isUploading)
+ Positioned.fill(
+ child: Container(
+ decoration: BoxDecoration(
+ color: Colors.black.withOpacity(0.6),
+ ),
+ child: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ SizedBox(
+ width: 24,
+ height: 24,
+ child: CircularProgressIndicator(
+ color: Colors.white,
+ strokeWidth: 2,
+ value: _uploadAnimation.value == 1.0
+ ? null
+ : _uploadAnimation.value,
+ ),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ 'Uploading...',
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 10,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ ],
+ ),
+ ),
),
),
- const Spacer(),
- Padding(
- padding: const EdgeInsets.only(right: 10.0),
- child: Button.filled(
- height: 30.0,
- width: 140.0,
- onPressed: _pickImage,
- label: 'Choose Photo',
- fontSize: 12.0,
- borderRadius: 5.0,
+ // Overlay gradient for better button visibility
+ if ((imagePath != null ||
+ uploadedImageUrl != null ||
+ (hasInitialImage && widget.initialImageUrl != null)) &&
+ !isUploading)
+ Positioned.fill(
+ child: Container(
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ colors: [
+ Colors.transparent,
+ Colors.black.withOpacity(0.3),
+ ],
+ begin: Alignment.topCenter,
+ end: Alignment.bottomCenter,
+ ),
+ ),
),
),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildPlaceholder() {
+ return Container(
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ colors: [
+ AppColors.primary.withOpacity(0.1),
+ AppColors.primary.withOpacity(0.05),
+ ],
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ ),
+ ),
+ child: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(
+ Icons.add_photo_alternate_outlined,
+ size: 32,
+ color: AppColors.primary.withOpacity(0.6),
+ ),
+ const SizedBox(height: 4),
+ Text(
+ 'Photo',
+ style: TextStyle(
+ fontSize: 10,
+ color: AppColors.primary.withOpacity(0.6),
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildActionButton() {
+ bool hasImage = imagePath != null ||
+ uploadedImageUrl != null ||
+ (hasInitialImage && widget.initialImageUrl != null);
+
+ return Container(
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(12),
+ gradient: LinearGradient(
+ colors: [
+ AppColors.primary,
+ AppColors.primary.withOpacity(0.8),
+ ],
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ ),
+ boxShadow: [
+ BoxShadow(
+ color: AppColors.primary.withOpacity(0.3),
+ blurRadius: 8,
+ offset: const Offset(0, 4),
+ ),
+ ],
+ ),
+ child: Material(
+ color: Colors.transparent,
+ child: InkWell(
+ onTap: isUploading ? null : _pickImage,
+ onHover: (hover) {
+ setState(() {
+ isHovering = hover;
+ });
+ },
+ borderRadius: BorderRadius.circular(12),
+ child: AnimatedContainer(
+ duration: const Duration(milliseconds: 200),
+ padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(12),
+ color: isHovering
+ ? Colors.white.withOpacity(0.1)
+ : Colors.transparent,
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(
+ isUploading
+ ? Icons.cloud_upload_outlined
+ : hasImage
+ ? Icons.edit_outlined
+ : Icons.add_photo_alternate_outlined,
+ color: Colors.white,
+ size: 18,
+ ),
+ const SizedBox(width: 8),
+ Text(
+ isUploading
+ ? 'Uploading...'
+ : hasImage
+ ? 'Change Photo'
+ : 'Choose Photo',
+ style: const TextStyle(
+ color: Colors.white,
+ fontSize: 14,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildUploadButton() {
+ if (!widget.autoUpload &&
+ imagePath != null &&
+ uploadedImageUrl == null &&
+ !isUploading) {
+ return Padding(
+ padding: const EdgeInsets.only(top: 12),
+ child: Container(
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(12),
+ gradient: LinearGradient(
+ colors: [
+ Colors.green.shade600,
+ Colors.green.shade500,
+ ],
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ ),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.green.withOpacity(0.3),
+ blurRadius: 8,
+ offset: const Offset(0, 4),
+ ),
],
),
+ child: Material(
+ color: Colors.transparent,
+ child: InkWell(
+ onTap: () => _uploadImage(imagePath!),
+ borderRadius: BorderRadius.circular(12),
+ child: Container(
+ padding:
+ const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(
+ Icons.cloud_upload_outlined,
+ color: Colors.white,
+ size: 16,
+ ),
+ const SizedBox(width: 8),
+ Text(
+ 'Upload to Server',
+ style: const TextStyle(
+ color: Colors.white,
+ fontSize: 12,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
),
- ],
+ );
+ }
+ return const SizedBox.shrink();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return BlocListener(
+ listener: (context, state) {
+ state.when(
+ initial: () {},
+ loading: () {
+ if (!isUploading) {
+ setState(() {
+ isUploading = true;
+ });
+ _uploadController.repeat();
+ }
+ },
+ success: (fileData) {
+ setState(() {
+ isUploading = false;
+ uploadedImageUrl = fileData.fileUrl;
+ });
+ _uploadController.reset();
+
+ if (widget.onUploaded != null) {
+ widget.onUploaded!(fileData.fileUrl);
+ }
+
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Row(
+ children: [
+ Icon(Icons.check_circle, color: Colors.white),
+ SizedBox(width: 8),
+ Text('Image uploaded successfully!'),
+ ],
+ ),
+ backgroundColor: Colors.green,
+ behavior: SnackBarBehavior.floating,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(10),
+ ),
+ duration: Duration(seconds: 2),
+ ),
+ );
+ },
+ error: (message) {
+ setState(() {
+ isUploading = false;
+ });
+ _uploadController.reset();
+
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Row(
+ children: [
+ Icon(Icons.error_outline, color: Colors.white),
+ SizedBox(width: 8),
+ Expanded(child: Text('Upload failed: $message')),
+ ],
+ ),
+ backgroundColor: Colors.red,
+ behavior: SnackBarBehavior.floating,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(10),
+ ),
+ duration: Duration(seconds: 3),
+ ),
+ );
+ },
+ );
+ },
+ child: FadeTransition(
+ opacity: _fadeAnimation,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ if (widget.showLabel) ...[
+ Text(
+ widget.label,
+ style: const TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.w700,
+ color: Colors.black87,
+ ),
+ ),
+ const SpaceHeight(16.0),
+ ],
+ ScaleTransition(
+ scale: _scaleAnimation,
+ child: Container(
+ padding: const EdgeInsets.all(20.0),
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(24.0),
+ color: Colors.white,
+ border: Border.all(
+ color: isUploading
+ ? Colors.orange.withOpacity(0.5)
+ : uploadedImageUrl != null
+ ? Colors.green.withOpacity(0.5)
+ : AppColors.primary.withOpacity(0.2),
+ width: 1.5,
+ ),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withOpacity(0.05),
+ blurRadius: 20,
+ offset: const Offset(0, 10),
+ ),
+ ],
+ ),
+ child: Column(
+ children: [
+ _buildImageContainer(),
+ const SizedBox(height: 20),
+ _buildActionButton(),
+ _buildUploadButton(),
+ if ((imagePath != null ||
+ uploadedImageUrl != null ||
+ (hasInitialImage &&
+ widget.initialImageUrl != null)) &&
+ !isUploading) ...[
+ const SizedBox(height: 12),
+ TextButton.icon(
+ onPressed: () {
+ setState(() {
+ imagePath = null;
+ hasInitialImage = false;
+ uploadedImageUrl = null;
+ });
+ widget.onChanged(null);
+ if (widget.onUploaded != null) {
+ widget.onUploaded!(null);
+ }
+ },
+ icon: Icon(
+ Icons.delete_outline,
+ size: 16,
+ color: Colors.red.shade400,
+ ),
+ label: Text(
+ 'Remove Photo',
+ style: TextStyle(
+ color: Colors.red.shade400,
+ fontSize: 12,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ ),
+ ],
+ // Upload status indicator
+ if (uploadedImageUrl != null && !isUploading) ...[
+ const SizedBox(height: 8),
+ Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 12, vertical: 6),
+ decoration: BoxDecoration(
+ color: Colors.green.withOpacity(0.1),
+ borderRadius: BorderRadius.circular(20),
+ border:
+ Border.all(color: Colors.green.withOpacity(0.3)),
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(
+ Icons.cloud_done_outlined,
+ size: 14,
+ color: Colors.green.shade600,
+ ),
+ const SizedBox(width: 6),
+ Text(
+ 'Uploaded',
+ style: TextStyle(
+ fontSize: 10,
+ fontWeight: FontWeight.w600,
+ color: Colors.green.shade600,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
);
}
}
+
+/// Cara menggunakan widget ini:
+///
+/// ```dart
+/// // 1. Basic usage tanpa auto upload
+/// ImagePickerWidget(
+/// label: 'Product Image',
+/// onChanged: (file) {
+/// // Handle selected file
+/// print('Selected file: ${file?.path}');
+/// },
+/// onUploaded: (url) {
+/// // Handle uploaded URL
+/// print('Uploaded URL: $url');
+/// },
+/// )
+///
+/// // 2. Auto upload setelah memilih gambar
+/// ImagePickerWidget(
+/// label: 'Profile Picture',
+/// autoUpload: true,
+/// onChanged: (file) => setState(() => selectedFile = file),
+/// onUploaded: (url) => setState(() => profileImageUrl = url),
+/// )
+///
+/// // 3. Dengan initial image
+/// ImagePickerWidget(
+/// label: 'Banner Image',
+/// initialImageUrl: existingImageUrl,
+/// onChanged: (file) => handleFileChange(file),
+/// onUploaded: (url) => handleUploadSuccess(url),
+/// )
+/// ```
+///
+/// Pastikan untuk wrap widget ini dengan BlocProvider:
+/// ```dart
+/// BlocProvider(
+/// create: (context) => UploadFileBloc(
+/// context.read(),
+/// ),
+/// child: ImagePickerWidget(...),
+/// )
+/// ```
\ No newline at end of file
diff --git a/lib/core/components/search_input.dart b/lib/core/components/search_input.dart
index 0be828b..d07a4c0 100644
--- a/lib/core/components/search_input.dart
+++ b/lib/core/components/search_input.dart
@@ -2,8 +2,6 @@ import 'package:flutter/material.dart';
import '../constants/colors.dart';
-
-
class SearchInput extends StatelessWidget {
final TextEditingController controller;
final Function(String value)? onChanged;
diff --git a/lib/core/constants/colors.dart b/lib/core/constants/colors.dart
index 3bfc2c3..8c0311b 100644
--- a/lib/core/constants/colors.dart
+++ b/lib/core/constants/colors.dart
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
class AppColors {
/// primary = #3949AB
- static const Color primary = Color(0xff6466f1);
+ static const Color primary = Color(0xff36175e);
/// grey = #B7B7B7
static const Color grey = Color(0xffB7B7B7);
@@ -18,6 +18,7 @@ class AppColors {
/// white = #FFFFFF
static const Color white = Color(0xffFFFFFF);
+ static const Color whiteText = Color(0xfff1eaf9);
/// green = #50C474
static const Color green = Color(0xff50C474);
@@ -36,4 +37,10 @@ class AppColors {
/// stroke = #EFF0F6
static const Color stroke = Color(0xffEFF0F6);
+
+ static const Color background = Color.fromARGB(255, 241, 241, 241);
+
+ static const Color primaryLight = Color(0xFF5A3E8A);
+ static const Color greyLight = Color(0xFFE0E0E0);
+ static const Color greyDark = Color(0xFF707070);
}
diff --git a/lib/core/constants/theme.dart b/lib/core/constants/theme.dart
new file mode 100644
index 0000000..c749181
--- /dev/null
+++ b/lib/core/constants/theme.dart
@@ -0,0 +1,41 @@
+import 'package:enaklo_pos/core/assets/fonts.gen.dart';
+import 'package:enaklo_pos/core/constants/colors.dart';
+import 'package:flutter/material.dart';
+
+ThemeData getApplicationTheme = ThemeData(
+ primaryColor: AppColors.primary,
+ scaffoldBackgroundColor: AppColors.white,
+ appBarTheme: AppBarTheme(
+ color: AppColors.white,
+ elevation: 0,
+ titleTextStyle: TextStyle(
+ color: AppColors.primary,
+ fontSize: 16.0,
+ fontWeight: FontWeight.w500,
+ ),
+ iconTheme: const IconThemeData(
+ color: AppColors.primary,
+ ),
+ ),
+ fontFamily: FontFamily.quicksand,
+ colorScheme: ColorScheme.fromSeed(seedColor: AppColors.primary),
+ useMaterial3: true,
+ inputDecorationTheme: InputDecorationTheme(
+ contentPadding: const EdgeInsets.symmetric(horizontal: 16.0),
+ hintStyle: const TextStyle(
+ color: AppColors.grey,
+ ),
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(8.0),
+ borderSide: BorderSide(color: AppColors.primary),
+ ),
+ focusedBorder: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(8.0),
+ borderSide: BorderSide(color: AppColors.primary),
+ ),
+ enabledBorder: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(8.0),
+ borderSide: BorderSide(color: AppColors.primary),
+ ),
+ ),
+);
diff --git a/lib/core/constants/variables.dart b/lib/core/constants/variables.dart
index d0717c9..b5b9b83 100644
--- a/lib/core/constants/variables.dart
+++ b/lib/core/constants/variables.dart
@@ -2,5 +2,6 @@ class Variables {
static const String appName = 'POS Kasir Resto App';
static const String apiVersion = 'v1';
// static const String baseUrl = 'http://192.168.1.202:8000';
- static const String baseUrl = 'https://pos-app-tablet.enaklo.co.id';
+ static const String baseUrl = 'https://enaklo-pos-be.altru.id';
+ static const int defaultLimit = 10;
}
diff --git a/lib/core/extensions/string_ext.dart b/lib/core/extensions/string_ext.dart
index 4ed973a..cdb7ae7 100644
--- a/lib/core/extensions/string_ext.dart
+++ b/lib/core/extensions/string_ext.dart
@@ -15,4 +15,12 @@ extension StringExt on String {
decimalDigits: 0,
).format(parsedValue);
}
+
+ String toTitleCase() {
+ if (isEmpty) return '';
+ return split(' ').map((word) {
+ if (word.isEmpty) return '';
+ return word[0].toUpperCase() + word.substring(1).toLowerCase();
+ }).join(' ');
+ }
}
diff --git a/lib/core/function/app_function.dart b/lib/core/function/app_function.dart
new file mode 100644
index 0000000..908d4ef
--- /dev/null
+++ b/lib/core/function/app_function.dart
@@ -0,0 +1,183 @@
+import 'dart:developer';
+import 'dart:typed_data';
+
+import 'package:barcode_image/barcode_image.dart';
+import 'package:enaklo_pos/core/extensions/string_ext.dart';
+import 'package:enaklo_pos/core/utils/printer_service.dart';
+import 'package:enaklo_pos/data/dataoutputs/print_dataoutputs.dart';
+import 'package:enaklo_pos/data/datasources/auth_local_datasource.dart';
+import 'package:enaklo_pos/data/datasources/outlet_local_datasource.dart';
+import 'package:enaklo_pos/data/datasources/product_local_datasource.dart';
+import 'package:enaklo_pos/data/datasources/settings_local_datasource.dart';
+import 'package:enaklo_pos/data/models/response/order_response_model.dart';
+import 'package:enaklo_pos/data/type/bussines_type.dart';
+import 'package:enaklo_pos/presentation/home/models/product_quantity.dart';
+import 'package:flutter/material.dart';
+import 'package:barcode/barcode.dart';
+import 'package:image/image.dart' as img;
+
+Future onPrint(
+ BuildContext context, {
+ required List productQuantity,
+ required Order order,
+}) async {
+ final outlet = await OutletLocalDatasource().get();
+ if (outlet.businessType == BusinessType.restaurant) {
+ final checkerPrinter =
+ await ProductLocalDatasource.instance.getPrinterByCode('checker');
+ final kitchenPrinter =
+ await ProductLocalDatasource.instance.getPrinterByCode('kitchen');
+ final barPrinter =
+ await ProductLocalDatasource.instance.getPrinterByCode('bar');
+
+ final authData = await AuthLocalDataSource().getAuthData();
+
+ // Checker printer
+ if (checkerPrinter != null) {
+ try {
+ final printValue = await PrintDataoutputs.instance.printChecker(
+ productQuantity,
+ order.tableNumber ?? "",
+ order.orderNumber ?? "",
+ authData.user?.name ?? "",
+ checkerPrinter.paper.toIntegerFromText,
+ order.orderType ?? "",
+ );
+
+ await PrinterService()
+ // ignore: use_build_context_synchronously
+ .printWithPrinter(checkerPrinter, printValue, context);
+ } catch (e) {
+ log("Error printing checker: $e");
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text('Error printing checker: $e')),
+ );
+ }
+ }
+
+ // Kitchen printer
+ if (kitchenPrinter != null) {
+ try {
+ final printValue = await PrintDataoutputs.instance.printKitchen(
+ productQuantity,
+ order.tableNumber!,
+ order.orderNumber ?? "",
+ authData.user?.name ?? "",
+ kitchenPrinter.paper.toIntegerFromText,
+ order.orderType ?? "",
+ );
+
+ await PrinterService()
+ .printWithPrinter(kitchenPrinter, printValue, context);
+ } catch (e) {
+ log("Error printing kitchen order: $e");
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text('Error printing kitchen order: $e')),
+ );
+ }
+ }
+
+ // Bar printer
+ if (barPrinter != null) {
+ try {
+ final printValue = await PrintDataoutputs.instance.printBar(
+ productQuantity,
+ order.tableNumber ?? "",
+ order.orderNumber ?? "",
+ authData.user?.name ?? "",
+ barPrinter.paper.toIntegerFromText,
+ order.orderType ?? "",
+ );
+
+ await PrinterService()
+ .printWithPrinter(barPrinter, printValue, context);
+ } catch (e) {
+ log("Error printing bar order: $e");
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text('Error printing bar order: $e')),
+ );
+ }
+ }
+ }
+
+ if (outlet.businessType == BusinessType.ticketing) {
+ final ticketPrinter =
+ await ProductLocalDatasource.instance.getPrinterByCode('ticket');
+
+ final barcode = await generateBarcodeAsUint8List(order.orderNumber ?? "");
+
+ if (ticketPrinter != null) {
+ try {
+ final printValue = await PrintDataoutputs.instance.printTicket(
+ order.totalAmount ?? 0,
+ barcode,
+ ticketPrinter.paper.toIntegerFromText,
+ );
+
+ await PrinterService()
+ // ignore: use_build_context_synchronously
+ .printWithPrinter(ticketPrinter, printValue, context);
+ } catch (e) {
+ log("Error printing ticket: $e");
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text('Error printing ticket: $e')),
+ );
+ }
+ }
+ }
+}
+
+Future onPrintRecipt(
+ context, {
+ required Order order,
+ required String paymentMethod,
+ required int nominalBayar,
+ required int kembalian,
+}) async {
+ final receiptPrinter =
+ await ProductLocalDatasource.instance.getPrinterByCode('receipt');
+ final authData = await AuthLocalDataSource().getAuthData();
+ final settings = await SettingsLocalDatasource().getTax();
+ final outlet = await OutletLocalDatasource().get();
+
+ if (receiptPrinter != null) {
+ try {
+ final printValue = await PrintDataoutputs.instance.printOrderV4(
+ order,
+ authData.user?.name ?? "",
+ paymentMethod,
+ nominalBayar,
+ kembalian,
+ settings.value,
+ receiptPrinter.paper.toIntegerFromText,
+ order.orderType ?? "",
+ outlet,
+ );
+ await PrinterService()
+ .printWithPrinter(receiptPrinter, printValue, context);
+ } catch (e) {
+ log("Error printing receipt order: $e");
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text('Error printing receipt order: $e')),
+ );
+ }
+ }
+}
+
+Future generateBarcodeAsUint8List(String data) async {
+ // 1. Buat barcode instance (code128, qrCode, dll)
+ final barcode = Barcode.code128();
+
+ // 2. Siapkan canvas image dari package `image`
+ final image = img.Image(width: 600, height: 200);
+
+ // 3. Gambar barcode ke canvas menggunakan barcode_image
+ drawBarcode(
+ image,
+ barcode,
+ data,
+ );
+
+ // 4. Encode image ke Uint8List PNG
+ return Uint8List.fromList(img.encodePng(image));
+}
diff --git a/lib/core/network/dio_client.dart b/lib/core/network/dio_client.dart
new file mode 100644
index 0000000..309d3b5
--- /dev/null
+++ b/lib/core/network/dio_client.dart
@@ -0,0 +1,181 @@
+import 'package:awesome_dio_interceptor/awesome_dio_interceptor.dart';
+import 'package:dio/dio.dart';
+import 'package:enaklo_pos/core/extensions/build_context_ext.dart';
+import 'package:enaklo_pos/presentation/auth/login_page.dart';
+import 'package:flutter/material.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+class DioClient {
+ static final Dio _dio = Dio(BaseOptions(
+ connectTimeout: const Duration(seconds: 10),
+ receiveTimeout: const Duration(seconds: 10),
+ headers: {
+ 'Accept': 'application/json',
+ },
+ ))
+ ..interceptors.add(AuthInterceptor())
+ ..interceptors.add(
+ AwesomeDioInterceptor(
+ logRequestTimeout: true,
+ logRequestHeaders: true,
+ logResponseHeaders: true,
+ ),
+ );
+
+ static Dio get instance => _dio;
+}
+
+class AuthInterceptor extends Interceptor {
+ static final GlobalKey navigatorKey =
+ GlobalKey();
+
+ @override
+ void onRequest(
+ RequestOptions options, RequestInterceptorHandler handler) async {
+ // Add token to request headers
+ final prefs = await SharedPreferences.getInstance();
+ final token = prefs.getString('auth_token');
+
+ if (token != null) {
+ options.headers['Authorization'] = 'Bearer $token';
+ }
+
+ handler.next(options);
+ }
+
+ @override
+ void onResponse(Response response, ResponseInterceptorHandler handler) {
+ handler.next(response);
+ }
+
+ @override
+ void onError(DioException err, ErrorInterceptorHandler handler) async {
+ // Check if error is 401 (Unauthorized) - token expired
+ if (err.response?.statusCode == 401) {
+ await _handleTokenExpired();
+ }
+
+ handler.next(err);
+ }
+
+ Future _handleTokenExpired() async {
+ // Clear stored token
+ final prefs = await SharedPreferences.getInstance();
+ await prefs.remove('auth_token');
+ await prefs.remove('refresh_token');
+ await prefs.clear(); // Optional: clear all user data
+
+ // Navigate to login page
+ final context = navigatorKey.currentContext;
+ if (context != null) {
+ // Option 1: Navigate and remove all previous routes
+ context.pushReplacement(LoginPage());
+
+ // Option 2: If using GoRouter, uncomment below:
+ // GoRouter.of(context).go('/login');
+
+ // Option 3: If using custom routing, uncomment below:
+ // Navigator.of(context).pushAndRemoveUntil(
+ // MaterialPageRoute(builder: (context) => LoginPage()),
+ // (route) => false,
+ // );
+ }
+ }
+}
+
+// Alternative: Dengan Refresh Token Logic
+class AuthInterceptorWithRefresh extends Interceptor {
+ static final GlobalKey navigatorKey =
+ GlobalKey();
+
+ @override
+ void onRequest(
+ RequestOptions options, RequestInterceptorHandler handler) async {
+ final prefs = await SharedPreferences.getInstance();
+ final token = prefs.getString('auth_token');
+
+ if (token != null) {
+ options.headers['Authorization'] = 'Bearer $token';
+ }
+
+ handler.next(options);
+ }
+
+ @override
+ void onError(DioException err, ErrorInterceptorHandler handler) async {
+ if (err.response?.statusCode == 401) {
+ // Try refresh token first
+ final success = await _tryRefreshToken();
+
+ if (success) {
+ // Retry the original request
+ final response = await _retryRequest(err.requestOptions);
+ handler.resolve(response);
+ return;
+ } else {
+ // Refresh failed, redirect to login
+ await _handleTokenExpired();
+ }
+ }
+
+ handler.next(err);
+ }
+
+ Future _tryRefreshToken() async {
+ try {
+ final prefs = await SharedPreferences.getInstance();
+ final refreshToken = prefs.getString('refresh_token');
+
+ if (refreshToken == null) return false;
+
+ final response = await Dio().post(
+ 'YOUR_REFRESH_TOKEN_ENDPOINT',
+ data: {'refresh_token': refreshToken},
+ );
+
+ if (response.statusCode == 200) {
+ final newToken = response.data['access_token'];
+ final newRefreshToken = response.data['refresh_token'];
+
+ await prefs.setString('auth_token', newToken);
+ await prefs.setString('refresh_token', newRefreshToken);
+
+ return true;
+ }
+ } catch (e) {
+ print('Refresh token failed: $e');
+ }
+
+ return false;
+ }
+
+ Future _retryRequest(RequestOptions options) async {
+ final prefs = await SharedPreferences.getInstance();
+ final token = prefs.getString('auth_token');
+
+ options.headers['Authorization'] = 'Bearer $token';
+
+ return await DioClient.instance.request(
+ options.path,
+ options: Options(
+ method: options.method,
+ headers: options.headers,
+ ),
+ data: options.data,
+ queryParameters: options.queryParameters,
+ );
+ }
+
+ Future _handleTokenExpired() async {
+ final prefs = await SharedPreferences.getInstance();
+ await prefs.clear();
+
+ final context = navigatorKey.currentContext;
+ if (context != null) {
+ Navigator.of(context).pushNamedAndRemoveUntil(
+ '/login',
+ (route) => false,
+ );
+ }
+ }
+}
diff --git a/lib/core/utils/item_sales_invoice.dart b/lib/core/utils/item_sales_invoice.dart
index 4b46c87..751c321 100644
--- a/lib/core/utils/item_sales_invoice.dart
+++ b/lib/core/utils/item_sales_invoice.dart
@@ -38,7 +38,7 @@ class ItemSalesInvoice {
return HelperPdfService.saveDocument(
name:
- 'Enaklo POS | Item Sales Report | ${DateTime.now().millisecondsSinceEpoch}.pdf',
+ 'Apskel POS | Item Sales Report | ${DateTime.now().millisecondsSinceEpoch}.pdf',
pdf: pdf);
}
@@ -48,7 +48,7 @@ class ItemSalesInvoice {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 1 * PdfPageFormat.cm),
- Text('Enaklo POS | Item Sales Report',
+ Text('Apskel POS | Item Sales Report',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
diff --git a/lib/core/utils/revenue_invoice.dart b/lib/core/utils/revenue_invoice.dart
index 4471e9a..42ca4e6 100644
--- a/lib/core/utils/revenue_invoice.dart
+++ b/lib/core/utils/revenue_invoice.dart
@@ -22,13 +22,14 @@ class RevenueInvoice {
log("Starting PDF generation for summary report");
log("Summary model: ${summaryModel.toMap()}");
log("Search date formatted: $searchDateFormatted");
-
+
final pdf = Document();
log("PDF document created");
-
+
// Load logo image
log("Loading logo image...");
- final ByteData dataImage = await rootBundle.load('assets/images/logo.png');
+ final ByteData dataImage =
+ await rootBundle.load('assets/images/logo.png');
final Uint8List bytes = dataImage.buffer.asUint8List();
final image = pw.MemoryImage(bytes);
log("Logo image loaded successfully, size: ${bytes.length} bytes");
@@ -49,7 +50,7 @@ class RevenueInvoice {
log("Saving PDF document...");
return HelperPdfService.saveDocument(
name:
- 'Enaklo POS | Summary Sales Report | ${DateTime.now().millisecondsSinceEpoch}.pdf',
+ 'Apskel POS | Summary Sales Report | ${DateTime.now().millisecondsSinceEpoch}.pdf',
pdf: pdf,
);
} catch (e) {
@@ -69,7 +70,7 @@ class RevenueInvoice {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 1 * PdfPageFormat.cm),
- Text('Enaklo POS | Summary Sales Report',
+ Text('Apskel POS | Summary Sales Report',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
@@ -93,7 +94,7 @@ class RevenueInvoice {
static Widget buildTotal(SummaryModel summaryModel) {
log("Building total section with summary model: ${summaryModel.toMap()}");
-
+
// Helper function to safely parse string to int
int safeParseInt(String? value) {
if (value == null || value.isEmpty) return 0;
@@ -104,7 +105,7 @@ class RevenueInvoice {
return 0;
}
}
-
+
return Container(
width: double.infinity,
child: Column(
@@ -125,7 +126,8 @@ class RevenueInvoice {
buildText(
title: 'Discount',
titleStyle: TextStyle(fontWeight: FontWeight.normal),
- value: "- ${safeParseInt(summaryModel.totalDiscount).currencyFormatRp}",
+ value:
+ "- ${safeParseInt(summaryModel.totalDiscount).currencyFormatRp}",
unite: true,
textStyle: TextStyle(
color: PdfColor.fromHex('#FF0000'),
@@ -147,7 +149,8 @@ class RevenueInvoice {
titleStyle: TextStyle(
fontWeight: FontWeight.normal,
),
- value: safeParseInt(summaryModel.totalServiceCharge).currencyFormatRp,
+ value:
+ safeParseInt(summaryModel.totalServiceCharge).currencyFormatRp,
unite: true,
),
Divider(),
diff --git a/lib/core/utils/transaction_sales_invoice.dart b/lib/core/utils/transaction_sales_invoice.dart
index f7a7b70..3e31f6e 100644
--- a/lib/core/utils/transaction_sales_invoice.dart
+++ b/lib/core/utils/transaction_sales_invoice.dart
@@ -5,7 +5,7 @@ import 'package:enaklo_pos/core/extensions/int_ext.dart';
import 'package:flutter/services.dart';
import 'package:enaklo_pos/core/utils/helper_pdf_service.dart';
-import 'package:enaklo_pos/data/models/response/order_remote_datasource.dart';
+import 'package:enaklo_pos/data/models/response/order_response_model.dart';
import 'package:pdf/widgets.dart';
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
@@ -13,7 +13,7 @@ import 'package:pdf/widgets.dart' as pw;
class TransactionSalesInvoice {
static late Font ttf;
static Future generate(
- List itemOrders, String searchDateFormatted) async {
+ List itemOrders, String searchDateFormatted) async {
final pdf = Document();
// var data = await rootBundle.load("assets/fonts/noto-sans.ttf");
// ttf = Font.ttf(data);
@@ -38,7 +38,7 @@ class TransactionSalesInvoice {
return HelperPdfService.saveDocument(
name:
- 'Enaklo POS | Transaction Sales Report | ${DateTime.now().millisecondsSinceEpoch}.pdf',
+ 'Apskel POS | Transaction Sales Report | ${DateTime.now().millisecondsSinceEpoch}.pdf',
pdf: pdf);
}
@@ -48,7 +48,7 @@ class TransactionSalesInvoice {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 1 * PdfPageFormat.cm),
- Text('Enaklo POS | Transaction Sales Report',
+ Text('Apskel POS | Transaction Sales Report',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
@@ -70,7 +70,7 @@ class TransactionSalesInvoice {
),
]);
- static Widget buildInvoice(List itemOrders) {
+ static Widget buildInvoice(List itemOrders) {
final headers = [
'Total',
'Sub Total',
@@ -81,12 +81,13 @@ class TransactionSalesInvoice {
];
final data = itemOrders.map((item) {
return [
- item.total!.currencyFormatRp,
- item.subTotal!.currencyFormatRp,
- item.tax!.currencyFormatRp,
- int.parse(item.discountAmount!.replaceAll('.00', '')).currencyFormatRp,
- item.serviceCharge!.currencyFormatRp,
- item.transactionTime!.toFormattedDate2(),
+ item.totalAmount!.currencyFormatRp,
+ item.subtotal!.currencyFormatRp,
+ item.taxAmount!.currencyFormatRp,
+ int.parse(item.discountAmount!.toString().replaceAll('.00', ''))
+ .currencyFormatRp,
+ 0,
+ item.createdAt!.toFormattedDate2(),
];
}).toList();
diff --git a/lib/data/dataoutputs/print_dataoutputs.dart b/lib/data/dataoutputs/print_dataoutputs.dart
index c071a1f..eba8df6 100644
--- a/lib/data/dataoutputs/print_dataoutputs.dart
+++ b/lib/data/dataoutputs/print_dataoutputs.dart
@@ -1,5 +1,7 @@
import 'dart:math';
+import 'package:enaklo_pos/data/models/response/order_response_model.dart';
+import 'package:enaklo_pos/presentation/home/models/outlet_model.dart';
import 'package:esc_pos_utils_plus/esc_pos_utils_plus.dart';
import 'package:flutter/services.dart';
import 'package:enaklo_pos/core/extensions/int_ext.dart';
@@ -35,7 +37,7 @@ class PrintDataoutputs {
final total = totalPrice + pajak;
bytes += generator.reset();
- bytes += generator.text('Enaklo POS',
+ bytes += generator.text('Apskel POS',
styles: const PosStyles(
bold: true,
align: PosAlign.center,
@@ -60,12 +62,12 @@ class PrintDataoutputs {
bytes += generator.row([
PosColumn(
text:
- '${product.product.price!.toIntegerFromText.currencyFormatRp} x ${product.quantity}',
+ '${product.product.price!.currencyFormatRp} x ${product.quantity}',
width: 8,
styles: const PosStyles(align: PosAlign.left),
),
PosColumn(
- text: '${product.product.price!.toIntegerFromText * product.quantity}'
+ text: '${product.product.price! * product.quantity}'
.toIntegerFromText
.currencyFormatRp,
width: 4,
@@ -202,7 +204,7 @@ class PrintDataoutputs {
// bytes += generator.feed(3);
// }
- bytes += generator.text('Enaklo POS',
+ bytes += generator.text('Apskel POS',
styles: const PosStyles(
bold: true,
align: PosAlign.center,
@@ -328,8 +330,7 @@ class PrintDataoutputs {
styles: const PosStyles(align: PosAlign.left),
),
PosColumn(
- text: (product.product.price!.toIntegerFromText * product.quantity)
- .currencyFormatRp,
+ text: (product.product.price! * product.quantity).currencyFormatRp,
width: 4,
styles: const PosStyles(align: PosAlign.right),
),
@@ -398,8 +399,7 @@ class PrintDataoutputs {
styles: const PosStyles(align: PosAlign.left),
),
PosColumn(
- text: (products[0].product.price!.toIntegerFromText *
- products[0].quantity)
+ text: (products[0].product.price! * products[0].quantity)
.currencyFormatRp,
width: 4,
styles: const PosStyles(align: PosAlign.right),
@@ -430,8 +430,7 @@ class PrintDataoutputs {
styles: const PosStyles(align: PosAlign.left),
),
PosColumn(
- text: (products[0].product.price!.toIntegerFromText *
- products[0].quantity)
+ text: (products[0].product.price! * products[0].quantity)
.currencyFormatRp,
width: 4,
styles: const PosStyles(align: PosAlign.right),
@@ -474,21 +473,21 @@ class PrintDataoutputs {
}
Future> printOrderV3(
- List products,
- int totalQuantity,
- int totalPrice,
- String paymentMethod,
- int nominalBayar,
- int kembalian,
- int subTotal,
- int discount,
- int pajak,
- int serviceCharge,
- String namaKasir,
- String customerName,
- int paper,
- {int taxPercentage = 11, int serviceChargePercentage = 5}
- ) async {
+ List products,
+ int totalQuantity,
+ int totalPrice,
+ String paymentMethod,
+ int nominalBayar,
+ int kembalian,
+ int subTotal,
+ int discount,
+ int pajak,
+ int serviceCharge,
+ String namaKasir,
+ String customerName,
+ int paper,
+ {int taxPercentage = 11,
+ int serviceChargePercentage = 5}) async {
List bytes = [];
final profile = await CapabilityProfile.load();
@@ -610,8 +609,8 @@ class PrintDataoutputs {
styles: const PosStyles(bold: true, align: PosAlign.left),
),
PosColumn(
- text: '${product.product.price!.toIntegerFromText * product.quantity}'
- .currencyFormatRpV2,
+ text:
+ '${product.product.price! * product.quantity}'.currencyFormatRpV2,
width: 4,
styles: const PosStyles(bold: true, align: PosAlign.right),
),
@@ -626,8 +625,7 @@ class PrintDataoutputs {
final subTotalPrice = products.fold(
0,
(previousValue, element) =>
- previousValue +
- (element.product.price!.toIntegerFromText * element.quantity));
+ previousValue + (element.product.price! * element.quantity));
bytes += generator.row([
PosColumn(
text: 'Subtotal $totalQuantity Product',
@@ -743,6 +741,265 @@ class PrintDataoutputs {
return bytes;
}
+ Future> printOrderV4(
+ Order order,
+ String chashierName,
+ String paymentMethod,
+ int nominalBayar,
+ int kembalian,
+ int taxPercentage,
+ int paper,
+ String orderType,
+ Outlet outlet,
+ ) async {
+ List bytes = [];
+
+ final profile = await CapabilityProfile.load();
+ final generator =
+ Generator(paper == 58 ? PaperSize.mm58 : PaperSize.mm80, profile);
+
+ bytes += generator.reset();
+
+ bytes += generator.text(outlet.name ?? "",
+ styles: const PosStyles(
+ bold: true,
+ align: PosAlign.center,
+ height: PosTextSize.size1,
+ width: PosTextSize.size1,
+ ));
+
+ bytes += generator.text(outlet.address ?? "",
+ styles: const PosStyles(bold: false, align: PosAlign.center));
+ bytes += generator.text(outlet.phoneNumber ?? "",
+ styles: const PosStyles(bold: false, align: PosAlign.center));
+
+ bytes += generator.text(
+ paper == 80
+ ? '------------------------------------------------'
+ : '--------------------------------',
+ styles: const PosStyles(bold: false, align: PosAlign.center));
+
+ bytes += generator.row([
+ PosColumn(
+ text: DateFormat('dd MMM yyyy').format(DateTime.now()),
+ width: 6,
+ styles: const PosStyles(align: PosAlign.left),
+ ),
+ PosColumn(
+ text: DateFormat('HH:mm').format(DateTime.now()),
+ width: 6,
+ styles: const PosStyles(align: PosAlign.right),
+ ),
+ ]);
+ bytes += generator.row([
+ PosColumn(
+ text: 'Receipt Number',
+ width: 6,
+ styles: const PosStyles(align: PosAlign.left),
+ ),
+ PosColumn(
+ text: 'JF-${DateFormat('yyyyMMddhhmm').format(DateTime.now())}',
+ width: 6,
+ styles: const PosStyles(align: PosAlign.right),
+ ),
+ ]);
+
+ bytes += generator.row([
+ PosColumn(
+ text: 'Order ID',
+ width: 6,
+ styles: const PosStyles(align: PosAlign.left),
+ ),
+ PosColumn(
+ text: Random().nextInt(100000).toString(),
+ width: 6,
+ styles: const PosStyles(align: PosAlign.right),
+ ),
+ ]);
+ bytes += generator.row([
+ PosColumn(
+ text: 'Bill Name',
+ width: 6,
+ styles: const PosStyles(align: PosAlign.left),
+ ),
+ PosColumn(
+ text: order.metadata?['customer_name'] ?? '',
+ width: 6,
+ styles: const PosStyles(align: PosAlign.right),
+ ),
+ ]);
+ bytes += generator.row([
+ PosColumn(
+ text: 'Collected By',
+ width: 6,
+ styles: const PosStyles(align: PosAlign.left),
+ ),
+ PosColumn(
+ text: chashierName,
+ width: 6,
+ styles: const PosStyles(align: PosAlign.right),
+ ),
+ ]);
+ bytes += generator.row([
+ PosColumn(
+ text: 'Pembayaran',
+ width: 8,
+ styles: const PosStyles(align: PosAlign.left),
+ ),
+ PosColumn(
+ text: paymentMethod,
+ width: 4,
+ styles: const PosStyles(align: PosAlign.right),
+ ),
+ ]);
+
+ bytes += generator.text(
+ paper == 80
+ ? '------------------------------------------------'
+ : '--------------------------------',
+ styles: const PosStyles(bold: false, align: PosAlign.center));
+ bytes += generator.text(orderType,
+ styles: const PosStyles(bold: true, align: PosAlign.center));
+ bytes += generator.text(
+ paper == 80
+ ? '------------------------------------------------'
+ : '--------------------------------',
+ styles: const PosStyles(bold: false, align: PosAlign.center));
+ for (final product in (order.orderItems ?? [])) {
+ bytes += generator.row([
+ PosColumn(
+ text: '${product.quantity} x ${product.productName}',
+ width: 8,
+ styles: const PosStyles(bold: true, align: PosAlign.left),
+ ),
+ PosColumn(
+ text: (product.totalPrice ?? 0).currencyFormatRpV2,
+ width: 4,
+ styles: const PosStyles(bold: true, align: PosAlign.right),
+ ),
+ ]);
+ }
+ bytes += generator.text(
+ paper == 80
+ ? '------------------------------------------------'
+ : '--------------------------------',
+ styles: const PosStyles(bold: false, align: PosAlign.center));
+
+ bytes += generator.row([
+ PosColumn(
+ text: 'Subtotal ${order.orderItems?.length ?? "0"} Product',
+ width: 6,
+ styles: const PosStyles(align: PosAlign.left),
+ ),
+ PosColumn(
+ text: (order.subtotal ?? 0).currencyFormatRpV2,
+ width: 6,
+ styles: const PosStyles(align: PosAlign.right),
+ ),
+ ]);
+
+ bytes += generator.row([
+ PosColumn(
+ text: 'Discount',
+ width: 6,
+ styles: const PosStyles(align: PosAlign.left),
+ ),
+ PosColumn(
+ text: (order.discountAmount ?? 0).currencyFormatRpV2,
+ width: 6,
+ styles: const PosStyles(align: PosAlign.right),
+ ),
+ ]);
+
+ // Only show tax if it's greater than 0
+ if ((order.taxAmount ?? 0) > 0) {
+ bytes += generator.row([
+ PosColumn(
+ text: 'Tax PB1 ($taxPercentage%)',
+ width: 6,
+ styles: const PosStyles(align: PosAlign.left),
+ ),
+ PosColumn(
+ text: (order.taxAmount ?? 0).currencyFormatRpV2,
+ width: 6,
+ styles: const PosStyles(align: PosAlign.right),
+ ),
+ ]);
+ }
+
+ // Only show service charge if it's greater than 0
+ // if (serviceCharge > 0) {
+ // bytes += generator.row([
+ // PosColumn(
+ // text: 'Service Charge($serviceChargePercentage%)',
+ // width: 6,
+ // styles: const PosStyles(align: PosAlign.left),
+ // ),
+ // PosColumn(
+ // text: serviceCharge.currencyFormatRpV2,
+ // width: 6,
+ // styles: const PosStyles(align: PosAlign.right),
+ // ),
+ // ]);
+ // }
+ bytes += generator.text(
+ paper == 80
+ ? '------------------------------------------------'
+ : '--------------------------------',
+ styles: const PosStyles(bold: false, align: PosAlign.center));
+ bytes += generator.row([
+ PosColumn(
+ text: 'Total',
+ width: 6,
+ styles: const PosStyles(bold: true, align: PosAlign.left),
+ ),
+ PosColumn(
+ text: '${order.totalAmount ?? ""}'.currencyFormatRpV2,
+ width: 6,
+ styles: const PosStyles(bold: true, align: PosAlign.right),
+ ),
+ ]);
+ bytes += generator.row([
+ PosColumn(
+ text: 'Dibayar',
+ width: 6,
+ styles: const PosStyles(align: PosAlign.left),
+ ),
+ PosColumn(
+ text: nominalBayar.currencyFormatRpV2,
+ width: 6,
+ styles: const PosStyles(align: PosAlign.right),
+ ),
+ ]);
+ bytes += generator.row([
+ PosColumn(
+ text: 'Kembali',
+ width: 6,
+ styles: const PosStyles(align: PosAlign.left),
+ ),
+ PosColumn(
+ text: kembalian.currencyFormatRpV2,
+ width: 6,
+ styles: const PosStyles(align: PosAlign.right),
+ ),
+ ]);
+ bytes += generator.text(
+ paper == 80
+ ? '------------------------------------------------'
+ : '--------------------------------',
+ styles: const PosStyles(bold: false, align: PosAlign.center));
+ // bytes += generator.text('Notes',
+ // styles: const PosStyles(bold: false, align: PosAlign.center));
+ // bytes += generator.text('Pass Wifi: fic14jilid2',
+ // styles: const PosStyles(bold: false, align: PosAlign.center));
+ // //terima kasih
+ // bytes += generator.text('Terima Kasih',
+ // styles: const PosStyles(bold: true, align: PosAlign.center));
+ paper == 80 ? bytes += generator.feed(3) : bytes += generator.feed(1);
+ bytes += generator.cut();
+ return bytes;
+ }
+
Future> printQRIS(
int totalPrice, Uint8List imageQris, int paper) async {
List bytes = [];
@@ -778,8 +1035,13 @@ class PrintDataoutputs {
return bytes;
}
- Future> printChecker(List products,
- String tableName, String draftName, String cashierName, int paper, String orderType) async {
+ Future> printChecker(
+ List products,
+ String tableName,
+ String draftName,
+ String cashierName,
+ int paper,
+ String orderType) async {
List bytes = [];
final profile = await CapabilityProfile.load();
@@ -908,8 +1170,13 @@ class PrintDataoutputs {
return bytes;
}
- Future> printKitchen(List products,
- String tableNumber, String draftName, String cashierName, int paper, String orderType) async {
+ Future> printKitchen(
+ List products,
+ String tableNumber,
+ String draftName,
+ String cashierName,
+ int paper,
+ String orderType) async {
List bytes = [];
final profile = await CapabilityProfile.load();
@@ -1134,4 +1401,39 @@ class PrintDataoutputs {
return bytes;
}
+
+ Future> printTicket(
+ int totalPrice, Uint8List imageQris, int paper) async {
+ List bytes = [];
+
+ final profile = await CapabilityProfile.load();
+ final generator =
+ Generator(paper == 58 ? PaperSize.mm58 : PaperSize.mm80, profile);
+
+ final img.Image? orginalImage = img.decodeImage(imageQris);
+ bytes += generator.reset();
+
+ // final Uint8List bytesData = data.buffer.asUint8List();
+ // final img.Image? orginalImage = img.decodeImage(bytesData);
+ // bytes += generator.reset();
+
+ bytes += generator.text('Scan Ticket',
+ styles: const PosStyles(bold: false, align: PosAlign.center));
+ bytes += generator.feed(2);
+ if (orginalImage != null) {
+ final img.Image grayscalledImage = img.grayscale(orginalImage);
+ final img.Image resizedImage =
+ img.copyResize(grayscalledImage, width: 240);
+ bytes += generator.imageRaster(resizedImage, align: PosAlign.center);
+ bytes += generator.feed(1);
+ }
+
+ bytes += generator.text('Price : ${totalPrice.currencyFormatRp}',
+ styles: const PosStyles(bold: false, align: PosAlign.center));
+
+ bytes += generator.feed(4);
+ bytes += generator.cut();
+
+ return bytes;
+ }
}
diff --git a/lib/data/datasources/analytic_remote_datasource.dart b/lib/data/datasources/analytic_remote_datasource.dart
new file mode 100644
index 0000000..b538034
--- /dev/null
+++ b/lib/data/datasources/analytic_remote_datasource.dart
@@ -0,0 +1,187 @@
+import 'dart:developer';
+
+import 'package:dartz/dartz.dart';
+import 'package:dio/dio.dart';
+import 'package:enaklo_pos/core/constants/variables.dart';
+import 'package:enaklo_pos/core/network/dio_client.dart';
+import 'package:enaklo_pos/data/datasources/auth_local_datasource.dart';
+import 'package:enaklo_pos/data/models/response/dashboard_analytic_response_model.dart';
+import 'package:enaklo_pos/data/models/response/payment_method_analytic_response_model.dart';
+import 'package:enaklo_pos/data/models/response/product_analytic_response_model.dart';
+import 'package:enaklo_pos/data/models/response/profit_loss_response_model.dart';
+import 'package:enaklo_pos/data/models/response/sales_analytic_response_model.dart';
+import 'package:intl/intl.dart';
+
+class AnalyticRemoteDatasource {
+ final Dio dio = DioClient.instance;
+
+ Future> getPaymentMethod({
+ required DateTime dateFrom,
+ required DateTime dateTo,
+ }) async {
+ final authData = await AuthLocalDataSource().getAuthData();
+ final headers = {
+ 'Authorization': 'Bearer ${authData.token}',
+ 'Accept': 'application/json',
+ };
+
+ try {
+ final response = await dio.get(
+ '${Variables.baseUrl}/api/v1/analytics/payment-methods',
+ queryParameters: {
+ 'date_from': DateFormat('dd-MM-yyyy').format(dateFrom),
+ 'date_to': DateFormat('dd-MM-yyyy').format(dateTo),
+ },
+ options: Options(headers: headers),
+ );
+
+ if (response.statusCode == 200) {
+ return right(PaymentMethodAnalyticResponseModel.fromMap(response.data));
+ } else {
+ return left('Terjadi Kesalahan, Coba lagi nanti.');
+ }
+ } on DioException catch (e) {
+ log('Dio error: ${e.message}');
+ return left(e.response?.data.toString() ?? e.message ?? 'Unknown error');
+ } catch (e) {
+ log('Unexpected error: $e');
+ return left('Unexpected error occurred');
+ }
+ }
+
+ Future> getSales({
+ required DateTime dateFrom,
+ required DateTime dateTo,
+ }) async {
+ final authData = await AuthLocalDataSource().getAuthData();
+ final headers = {
+ 'Authorization': 'Bearer ${authData.token}',
+ 'Accept': 'application/json',
+ };
+
+ try {
+ final response = await dio.get(
+ '${Variables.baseUrl}/api/v1/analytics/sales',
+ queryParameters: {
+ 'date_from': DateFormat('dd-MM-yyyy').format(dateFrom),
+ 'date_to': DateFormat('dd-MM-yyyy').format(dateTo),
+ },
+ options: Options(headers: headers),
+ );
+
+ if (response.statusCode == 200) {
+ return right(SalesAnalyticResponseModel.fromMap(response.data));
+ } else {
+ return left('Terjadi Kesalahan, Coba lagi nanti.');
+ }
+ } on DioException catch (e) {
+ log('Dio error: ${e.message}');
+ return left(e.response?.data.toString() ?? e.message ?? 'Unknown error');
+ } catch (e) {
+ log('Unexpected error: $e');
+ return left('Unexpected error occurred');
+ }
+ }
+
+ Future> getProduct({
+ required DateTime dateFrom,
+ required DateTime dateTo,
+ }) async {
+ final authData = await AuthLocalDataSource().getAuthData();
+ final headers = {
+ 'Authorization': 'Bearer ${authData.token}',
+ 'Accept': 'application/json',
+ };
+
+ try {
+ final response = await dio.get(
+ '${Variables.baseUrl}/api/v1/analytics/products',
+ queryParameters: {
+ 'date_from': DateFormat('dd-MM-yyyy').format(dateFrom),
+ 'date_to': DateFormat('dd-MM-yyyy').format(dateTo),
+ },
+ options: Options(headers: headers),
+ );
+
+ if (response.statusCode == 200) {
+ return right(ProductAnalyticResponseModel.fromMap(response.data));
+ } else {
+ return left('Terjadi Kesalahan, Coba lagi nanti.');
+ }
+ } on DioException catch (e) {
+ log('Dio error: ${e.message}');
+ return left(e.response?.data.toString() ?? e.message ?? 'Unknown error');
+ } catch (e) {
+ log('Unexpected error: $e');
+ return left('Unexpected error occurred');
+ }
+ }
+
+ Future> getDashboard({
+ required DateTime dateFrom,
+ required DateTime dateTo,
+ }) async {
+ final authData = await AuthLocalDataSource().getAuthData();
+ final headers = {
+ 'Authorization': 'Bearer ${authData.token}',
+ 'Accept': 'application/json',
+ };
+
+ try {
+ final response = await dio.get(
+ '${Variables.baseUrl}/api/v1/analytics/dashboard',
+ queryParameters: {
+ 'date_from': DateFormat('dd-MM-yyyy').format(dateFrom),
+ 'date_to': DateFormat('dd-MM-yyyy').format(dateTo),
+ },
+ options: Options(headers: headers),
+ );
+
+ if (response.statusCode == 200) {
+ return right(DashboardAnalyticResponseModel.fromMap(response.data));
+ } else {
+ return left('Terjadi Kesalahan, Coba lagi nanti.');
+ }
+ } on DioException catch (e) {
+ log('Dio error: ${e.message}');
+ return left(e.response?.data.toString() ?? e.message ?? 'Unknown error');
+ } catch (e) {
+ log('Unexpected error: $e');
+ return left('Unexpected error occurred');
+ }
+ }
+
+ Future> getProfitLoss({
+ required DateTime dateFrom,
+ required DateTime dateTo,
+ }) async {
+ final authData = await AuthLocalDataSource().getAuthData();
+ final headers = {
+ 'Authorization': 'Bearer ${authData.token}',
+ 'Accept': 'application/json',
+ };
+
+ try {
+ final response = await dio.get(
+ '${Variables.baseUrl}/api/v1/analytics/profit-loss',
+ queryParameters: {
+ 'date_from': DateFormat('dd-MM-yyyy').format(dateFrom),
+ 'date_to': DateFormat('dd-MM-yyyy').format(dateTo),
+ },
+ options: Options(headers: headers),
+ );
+
+ if (response.statusCode == 200) {
+ return right(ProfitLossResponseModel.fromMap(response.data));
+ } else {
+ return left('Terjadi Kesalahan, Coba lagi nanti.');
+ }
+ } on DioException catch (e) {
+ log('Dio error: ${e.message}');
+ return left(e.response?.data.toString() ?? e.message ?? 'Unknown error');
+ } catch (e) {
+ log('Unexpected error: $e');
+ return left('Unexpected error occurred');
+ }
+ }
+}
diff --git a/lib/data/datasources/auth_local_datasource.dart b/lib/data/datasources/auth_local_datasource.dart
index bf123d4..7488d36 100644
--- a/lib/data/datasources/auth_local_datasource.dart
+++ b/lib/data/datasources/auth_local_datasource.dart
@@ -1,10 +1,16 @@
+import 'dart:developer';
+
import 'package:enaklo_pos/data/models/response/auth_response_model.dart';
import 'package:shared_preferences/shared_preferences.dart';
class AuthLocalDataSource {
Future saveAuthData(AuthResponseModel authResponseModel) async {
- final prefs = await SharedPreferences.getInstance();
- await prefs.setString('auth_data', authResponseModel.toJson());
+ try {
+ final prefs = await SharedPreferences.getInstance();
+ await prefs.setString('auth_data', authResponseModel.toJson());
+ } catch (e) {
+ log('Error saving auth data: $e');
+ }
}
Future removeAuthData() async {
@@ -16,6 +22,8 @@ class AuthLocalDataSource {
final prefs = await SharedPreferences.getInstance();
final authData = prefs.getString('auth_data');
+ log('Auth data: $authData');
+
return AuthResponseModel.fromJson(authData!);
}
diff --git a/lib/data/datasources/auth_remote_datasource.dart b/lib/data/datasources/auth_remote_datasource.dart
index 5f34461..c589193 100644
--- a/lib/data/datasources/auth_remote_datasource.dart
+++ b/lib/data/datasources/auth_remote_datasource.dart
@@ -1,44 +1,67 @@
+import 'dart:developer';
+
import 'package:dartz/dartz.dart';
+import 'package:dio/dio.dart';
import 'package:enaklo_pos/core/constants/variables.dart';
+import 'package:enaklo_pos/core/network/dio_client.dart';
import 'package:enaklo_pos/data/datasources/auth_local_datasource.dart';
import 'package:enaklo_pos/data/models/response/auth_response_model.dart';
-import 'package:http/http.dart' as http;
class AuthRemoteDatasource {
+ final Dio dio = DioClient.instance;
Future> login(
String email, String password) async {
- final url = Uri.parse('${Variables.baseUrl}/api/login');
- final response = await http.post(
- url,
- body: {
- 'email': email,
- 'password': password,
- },
- );
+ final url = '${Variables.baseUrl}/api/v1/auth/login';
+ log(url);
- if (response.statusCode == 200) {
- return Right(AuthResponseModel.fromJson(response.body));
- } else {
- return const Left('Failed to login');
+ try {
+ final response = await dio.post(
+ url,
+ data: {
+ 'email': email,
+ 'password': password,
+ },
+ );
+
+ if (response.statusCode == 200) {
+ return Right(AuthResponseModel.fromMap(response.data['data']));
+ } else {
+ return const Left('Failed to login');
+ }
+ } on DioException catch (e) {
+ log("Dio error: ${e.message}");
+ return Left(e.response?.data['message'] ?? 'Login gagal');
+ } catch (e) {
+ log("Unexpected error: $e");
+ return const Left('Unexpected error occurred');
}
}
//logout
Future> logout() async {
- final authData = await AuthLocalDataSource().getAuthData();
- final url = Uri.parse('${Variables.baseUrl}/api/logout');
- final response = await http.post(
- url,
- headers: {
- 'Authorization': 'Bearer ${authData.token}',
- 'Accept': 'application/json',
- },
- );
+ try {
+ final authData = await AuthLocalDataSource().getAuthData();
+ final url = '${Variables.baseUrl}/api/v1/auth/logout';
- if (response.statusCode == 200) {
- return const Right(true);
- } else {
- return const Left('Failed to logout');
+ final response = await dio.post(
+ url,
+ options: Options(
+ headers: {
+ 'Authorization': 'Bearer ${authData.token}',
+ 'Accept': 'application/json',
+ },
+ ),
+ );
+
+ if (response.statusCode == 200) {
+ return const Right(true);
+ } else {
+ return const Left('Failed to logout');
+ }
+ } on DioException catch (e) {
+ return Left(e.response?.data['message'] ?? 'Logout gagal');
+ } catch (e) {
+ return const Left('Unexpected error occurred');
}
}
}
diff --git a/lib/data/datasources/category_remote_datasource.dart b/lib/data/datasources/category_remote_datasource.dart
index d81071a..1623496 100644
--- a/lib/data/datasources/category_remote_datasource.dart
+++ b/lib/data/datasources/category_remote_datasource.dart
@@ -1,27 +1,48 @@
import 'dart:developer';
import 'package:dartz/dartz.dart';
+import 'package:dio/dio.dart';
import 'package:enaklo_pos/core/constants/variables.dart';
+import 'package:enaklo_pos/core/network/dio_client.dart';
import 'package:enaklo_pos/data/datasources/auth_local_datasource.dart';
import 'package:enaklo_pos/data/models/response/category_response_model.dart';
-import 'package:http/http.dart' as http;
class CategoryRemoteDatasource {
- Future> getCategories() async {
+ final Dio dio = DioClient.instance;
+
+ Future> getCategories({
+ int page = 1,
+ int limit = 10,
+ bool isActive = true,
+ }) async {
final authData = await AuthLocalDataSource().getAuthData();
- final Map headers = {
+ final headers = {
'Authorization': 'Bearer ${authData.token}',
'Accept': 'application/json',
};
- final response = await http.get(
- Uri.parse('${Variables.baseUrl}/api/api-categories'),
- headers: headers);
- log(response.statusCode.toString());
- log(response.body);
- if (response.statusCode == 200) {
- return right(CategroyResponseModel.fromJson(response.body));
- } else {
- return left(response.body);
+
+ try {
+ final response = await dio.get(
+ '${Variables.baseUrl}/api/v1/categories',
+ queryParameters: {
+ 'page': page,
+ 'limit': limit,
+ 'is_active': isActive,
+ },
+ options: Options(headers: headers),
+ );
+
+ if (response.statusCode == 200) {
+ return right(CategoryResponseModel.fromMap(response.data));
+ } else {
+ return left(response.data.toString());
+ }
+ } on DioException catch (e) {
+ log('Dio error: ${e.message}');
+ return left(e.response?.data.toString() ?? e.message ?? 'Unknown error');
+ } catch (e) {
+ log('Unexpected error: $e');
+ return left('Unexpected error occurred');
}
}
}
diff --git a/lib/data/datasources/customer_remote_datasource.dart b/lib/data/datasources/customer_remote_datasource.dart
new file mode 100644
index 0000000..ec6fd05
--- /dev/null
+++ b/lib/data/datasources/customer_remote_datasource.dart
@@ -0,0 +1,101 @@
+import 'dart:developer';
+import 'package:dartz/dartz.dart';
+import 'package:dio/dio.dart';
+import 'package:enaklo_pos/core/network/dio_client.dart';
+import 'package:enaklo_pos/data/models/response/customer_response_model.dart';
+import '../../core/constants/variables.dart';
+import 'auth_local_datasource.dart';
+
+class CustomerRemoteDataSource {
+ final Dio dio = DioClient.instance;
+
+ Future> getCustomers({
+ int page = 1,
+ int limit = Variables.defaultLimit,
+ }) async {
+ try {
+ final authData = await AuthLocalDataSource().getAuthData();
+ final url = '${Variables.baseUrl}/api/v1/customers';
+
+ final response = await dio.get(
+ url,
+ queryParameters: {
+ 'page': page,
+ 'limit': limit,
+ 'organization_id': authData.user?.organizationId,
+ },
+ options: Options(
+ headers: {
+ 'Authorization': 'Bearer ${authData.token}',
+ 'Accept': 'application/json',
+ },
+ ),
+ );
+
+ if (response.statusCode == 200) {
+ return Right(CustomerResponseModel.fromMap(response.data));
+ } else {
+ return const Left('Failed to get customers');
+ }
+ } on DioException catch (e) {
+ log("Dio error: ${e.message}");
+ return Left(e.response?.data['message'] ?? 'Gagal mengambil customer');
+ } catch (e) {
+ log("Unexpected error: $e");
+ return const Left('Unexpected error occurred');
+ }
+ }
+
+ Future> createCustomer({
+ required String name,
+ required String phone,
+ required String address,
+ required String email,
+ required bool isActive,
+ }) async {
+ try {
+ final authData = await AuthLocalDataSource().getAuthData();
+ final url = '${Variables.baseUrl}/api/v1/customers';
+
+ Map data = {
+ 'name': name,
+ 'is_active': isActive,
+ };
+
+ if (phone.isNotEmpty) {
+ data['phone'] = phone;
+ }
+
+ if (address.isNotEmpty) {
+ data['address'] = address;
+ }
+
+ if (email.isNotEmpty) {
+ data['email'] = email;
+ }
+
+ final response = await dio.post(
+ url,
+ data: data,
+ options: Options(
+ headers: {
+ 'Authorization': 'Bearer ${authData.token}',
+ 'Accept': 'application/json',
+ },
+ ),
+ );
+
+ if (response.statusCode == 200 || response.statusCode == 201) {
+ return const Right(true);
+ } else {
+ return const Left('Failed to create customer ');
+ }
+ } on DioException catch (e) {
+ log("Dio error: ${e.message}");
+ return Left(e.response?.data['message'] ?? 'Gagal membuat pelanggan');
+ } catch (e) {
+ log("Unexpected error: $e");
+ return const Left('Unexpected error occurred');
+ }
+ }
+}
diff --git a/lib/data/datasources/delivery_local_datasource.dart b/lib/data/datasources/delivery_local_datasource.dart
new file mode 100644
index 0000000..5119edc
--- /dev/null
+++ b/lib/data/datasources/delivery_local_datasource.dart
@@ -0,0 +1,15 @@
+import 'package:enaklo_pos/core/assets/assets.gen.dart';
+import 'package:enaklo_pos/data/models/response/delivery_response_model.dart';
+
+List deliveries = [
+ DeliveryModel(
+ id: 'gojek',
+ name: 'Gojek',
+ imageUrl: Assets.images.gojek.path,
+ ),
+ DeliveryModel(
+ id: 'grab',
+ name: 'Grab',
+ imageUrl: Assets.images.grab.path,
+ ),
+];
diff --git a/lib/data/datasources/file_remote_datasource.dart b/lib/data/datasources/file_remote_datasource.dart
new file mode 100644
index 0000000..e46e70b
--- /dev/null
+++ b/lib/data/datasources/file_remote_datasource.dart
@@ -0,0 +1,53 @@
+import 'package:dartz/dartz.dart';
+import 'package:dio/dio.dart';
+import 'package:enaklo_pos/core/constants/variables.dart';
+import 'package:enaklo_pos/core/network/dio_client.dart';
+import 'package:enaklo_pos/data/datasources/auth_local_datasource.dart';
+import 'package:enaklo_pos/data/models/response/file_response_model.dart';
+
+class FileRemoteDataSource {
+ final Dio dio = DioClient.instance;
+
+ Future> uploadFile({
+ required String filePath,
+ required String fileType,
+ required String description,
+ }) async {
+ final url = '${Variables.baseUrl}/api/v1/files/upload';
+
+ try {
+ final authData = await AuthLocalDataSource().getAuthData();
+
+ // Membuat FormData
+ final formData = FormData.fromMap({
+ 'file': await MultipartFile.fromFile(filePath,
+ filename: filePath.split('/').last),
+ 'file_type': fileType,
+ 'description': description,
+ });
+
+ final response = await dio.post(
+ url,
+ data: formData,
+ options: Options(
+ headers: {
+ 'Authorization': 'Bearer ${authData.token}',
+ 'Accept': 'application/json',
+ // Content-Type otomatis diatur oleh Dio untuk FormData
+ },
+ ),
+ );
+
+ if (response.statusCode == 201 || response.statusCode == 200) {
+ // Misal response.data['url'] adalah URL file yang diupload
+ return Right(FileResponseModel.fromJson(response.data));
+ } else {
+ return Left('Upload gagal: ${response.statusMessage}');
+ }
+ } on DioException catch (e) {
+ return Left(e.response?.data['message'] ?? 'Upload gagal');
+ } catch (e) {
+ return Left('Unexpected error: $e');
+ }
+ }
+}
diff --git a/lib/data/datasources/order_remote_datasource.dart b/lib/data/datasources/order_remote_datasource.dart
index 22a85d7..fdb3675 100644
--- a/lib/data/datasources/order_remote_datasource.dart
+++ b/lib/data/datasources/order_remote_datasource.dart
@@ -1,17 +1,25 @@
+import 'dart:convert';
import 'dart:developer';
-import 'dart:convert';
-
import 'package:dartz/dartz.dart';
+import 'package:dio/dio.dart';
import 'package:enaklo_pos/core/constants/variables.dart';
+import 'package:enaklo_pos/core/network/dio_client.dart';
import 'package:enaklo_pos/data/datasources/auth_local_datasource.dart';
-import 'package:enaklo_pos/data/models/response/order_remote_datasource.dart';
+import 'package:enaklo_pos/data/models/request/payment_request.dart';
+import 'package:enaklo_pos/data/models/response/order_response_model.dart';
import 'package:enaklo_pos/data/models/response/payment_method_response_model.dart';
+import 'package:enaklo_pos/data/models/response/payment_methods_response_model.dart';
+import 'package:enaklo_pos/data/models/response/payment_response_model.dart';
import 'package:enaklo_pos/data/models/response/summary_response_model.dart';
import 'package:enaklo_pos/presentation/home/models/order_model.dart';
+import 'package:enaklo_pos/presentation/home/models/order_request.dart';
import 'package:http/http.dart' as http;
+import 'package:intl/intl.dart';
class OrderRemoteDatasource {
+ final Dio dio = DioClient.instance;
+
//save order to remote server
Future saveOrder(OrderModel orderModel) async {
final authData = await AuthLocalDataSource().getAuthData();
@@ -28,11 +36,11 @@ class OrderRemoteDatasource {
'Content-Type': 'application/json',
},
);
-
+
print("📥 HTTP Status Code: ${response.statusCode}");
print("📥 Response Body: ${response.body}");
print("📥 Response Headers: ${response.headers}");
-
+
if (response.statusCode == 200) {
print("✅ API call successful - Order saved to server");
return true;
@@ -69,7 +77,8 @@ class OrderRemoteDatasource {
print("✅ getOrderByRangeDate API call successful");
return Right(OrderResponseModel.fromJson(response.body));
} else {
- print("❌ getOrderByRangeDate API call failed - Status: ${response.statusCode}");
+ print(
+ "❌ getOrderByRangeDate API call failed - Status: ${response.statusCode}");
print("❌ Error Response: ${response.body}");
return const Left("Failed Load Data");
}
@@ -102,7 +111,8 @@ class OrderRemoteDatasource {
print("✅ getSummaryByRangeDate API call successful");
return Right(SummaryResponseModel.fromJson(response.body));
} else {
- print("❌ getSummaryByRangeDate API call failed - Status: ${response.statusCode}");
+ print(
+ "❌ getSummaryByRangeDate API call failed - Status: ${response.statusCode}");
print("❌ Error Response: ${response.body}");
return const Left("Failed Load Data");
}
@@ -112,7 +122,8 @@ class OrderRemoteDatasource {
}
}
- Future> getPaymentMethodByRangeDate(
+ Future>
+ getPaymentMethodByRangeDate(
String startDate,
String endDate,
) async {
@@ -134,7 +145,8 @@ class OrderRemoteDatasource {
print("✅ getPaymentMethodByRangeDate API call successful");
return Right(PaymentMethodResponseModel.fromJson(response.body));
} else {
- print("❌ getPaymentMethodByRangeDate API call failed - Status: ${response.statusCode}");
+ print(
+ "❌ getPaymentMethodByRangeDate API call failed - Status: ${response.statusCode}");
print("❌ Error Response: ${response.body}");
return const Left("Failed Load Payment Method Data");
}
@@ -162,16 +174,17 @@ class OrderRemoteDatasource {
'order_items': orderItems,
}),
);
-
+
print("📥 Add Order Items HTTP Status Code: ${response.statusCode}");
print("📥 Add Order Items Response Body: ${response.body}");
print("📥 Add Order Items Response Headers: ${response.headers}");
-
+
if (response.statusCode == 200) {
print("✅ addOrderItems API call successful");
return const Right(true);
} else {
- print("❌ addOrderItems API call failed - Status: ${response.statusCode}");
+ print(
+ "❌ addOrderItems API call failed - Status: ${response.statusCode}");
print("❌ Error Response: ${response.body}");
return Left("Failed to add order items: ${response.body}");
}
@@ -180,4 +193,432 @@ class OrderRemoteDatasource {
return Left("Failed: $e");
}
}
+
+ // New Api
+ Future> createOrder(
+ OrderRequestModel orderModel,
+ ) async {
+ final authData = await AuthLocalDataSource().getAuthData();
+ final url = '${Variables.baseUrl}/api/v1/orders';
+
+ try {
+ final response = await dio.post(
+ url,
+ data: orderModel.toMap(),
+ options: Options(
+ headers: {
+ 'Authorization': 'Bearer ${authData.token}',
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ ),
+ );
+
+ if (response.statusCode == 200) {
+ return Right(OrderDetailResponseModel.fromMap(response.data));
+ } else {
+ return const Left('Gagal membuat pesanan');
+ }
+ } on DioException catch (e) {
+ final errorMessage =
+ e.response?.data['message'] ?? 'Terjadi kesalahan, coba lagi nanti.';
+ log("💥 Dio error: ${e.message}");
+ log("💥 Dio response: ${e.response?.data}");
+ return Left(errorMessage);
+ } catch (e) {
+ log("💥 Unexpected error: $e");
+ return const Left('Terjadi kesalahan tak terduga');
+ }
+ }
+
+ Future> createPayment(
+ PaymentRequestModel orderModel) async {
+ final authData = await AuthLocalDataSource().getAuthData();
+ final url = '${Variables.baseUrl}/api/v1/payments';
+
+ try {
+ final response = await dio.post(
+ url,
+ data: orderModel.toMap(),
+ options: Options(
+ headers: {
+ 'Authorization': 'Bearer ${authData.token}',
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ ),
+ );
+
+ if (response.statusCode == 200) {
+ return Right(PaymentSuccessResponseModel.fromMap(response.data));
+ } else {
+ return const Left('Gagal membuat pembayaran');
+ }
+ } on DioException catch (e) {
+ final errorMessage =
+ e.response?.data['message'] ?? 'Terjadi kesalahan, coba lagi nanti.';
+ log("💥 Dio error: ${e.message}");
+ log("💥 Dio response: ${e.response?.data}");
+ return Left(errorMessage);
+ } catch (e) {
+ log("💥 Unexpected error: $e");
+ return const Left('Terjadi kesalahan tak terduga');
+ }
+ }
+
+ Future> getOrder({
+ int page = 1,
+ int limit = Variables.defaultLimit,
+ String status = 'completed',
+ required DateTime dateFrom,
+ required DateTime dateTo,
+ String? search,
+ }) async {
+ try {
+ final authData = await AuthLocalDataSource().getAuthData();
+
+ Map params = {
+ 'page': page,
+ 'limit': limit,
+ 'status': status,
+ 'date_from': DateFormat('dd-MM-yyyy').format(dateFrom),
+ 'date_to': DateFormat('dd-MM-yyyy').format(dateTo),
+ };
+
+ if (search != null && search.isNotEmpty) {
+ params['search'] = search;
+ }
+
+ final response = await dio.get(
+ '${Variables.baseUrl}/api/v1/orders',
+ queryParameters: params,
+ options: Options(
+ headers: {
+ 'Authorization': 'Bearer ${authData.token}',
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ ),
+ );
+
+ if (response.statusCode == 200) {
+ return Right(OrderResponseModel.fromMap(response.data));
+ } else {
+ log("❌ getOrderByRangeDate API call failed - Status: ${response.statusCode}");
+ return const Left("Failed Load Data");
+ }
+ } on DioException catch (e) {
+ final errorMessage = 'Something went wrong';
+ log("💥 Dio error: ${e.message}");
+ log("💥 Dio response: ${e.response?.data}");
+ return Left(errorMessage);
+ } catch (e) {
+ log("💥 Unexpected error: $e");
+ return Left("Unexpected Error: $e");
+ }
+ }
+
+ Future> getOrderById(
+ {required String orderId}) async {
+ try {
+ final authData = await AuthLocalDataSource().getAuthData();
+ final response = await dio.get(
+ '${Variables.baseUrl}/api/v1/orders/$orderId',
+ options: Options(
+ headers: {
+ 'Authorization': 'Bearer ${authData.token}',
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ ),
+ );
+
+ if (response.statusCode == 200) {
+ return Right(OrderDetailResponseModel.fromMap(response.data));
+ } else {
+ log("❌ OrderDetailResponseModel API call failed - Status: ${response.statusCode}");
+ return const Left("Failed Load Data");
+ }
+ } on DioException catch (e) {
+ final errorMessage = 'Something went wrong';
+ log("💥 Dio error: ${e.message}");
+ log("💥 Dio response: ${e.response?.data}");
+ return Left(errorMessage);
+ } catch (e) {
+ log("💥 Unexpected error: $e");
+ return Left("Unexpected Error: $e");
+ }
+ }
+
+ Future> createOrderWithPayment(
+ OrderRequestModel orderModel,
+ PaymentMethod payment,
+ ) async {
+ final authData = await AuthLocalDataSource().getAuthData();
+ final url = '${Variables.baseUrl}/api/v1/orders';
+
+ try {
+ final response = await dio.post(
+ url,
+ data: orderModel.toMap(),
+ options: Options(
+ headers: {
+ 'Authorization': 'Bearer ${authData.token}',
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ ),
+ );
+
+ if (response.statusCode == 200) {
+ final data = OrderDetailResponseModel.fromMap(response.data);
+ final orderData = data.data;
+ final paymentRequest = PaymentRequestModel(
+ orderId: orderData?.id,
+ amount: orderData?.totalAmount,
+ paymentMethodId: payment.id,
+ splitDescription: '',
+ splitNumber: 1,
+ splitTotal: 1,
+ paymentOrderItems: orderData?.orderItems
+ ?.map((item) => PaymentOrderItemModel(
+ amount: item.totalPrice,
+ orderItemId: item.id,
+ ))
+ .toList(),
+ );
+
+ createPayment(paymentRequest);
+ return Right(data);
+ } else {
+ return const Left('Gagal membuat pesanan');
+ }
+ } on DioException catch (e) {
+ final errorMessage =
+ e.response?.data['message'] ?? 'Terjadi kesalahan, coba lagi nanti.';
+ log("💥 Dio error: ${e.message}");
+ log("💥 Dio response: ${e.response?.data}");
+ return Left(errorMessage);
+ } catch (e) {
+ log("💥 Unexpected error: $e");
+ return const Left('Terjadi kesalahan tak terduga');
+ }
+ }
+
+ Future> addToOrder({
+ required String orderId,
+ required List orderItems,
+ }) async {
+ final authData = await AuthLocalDataSource().getAuthData();
+ final url = '${Variables.baseUrl}/api/v1/orders/$orderId/add-items';
+
+ try {
+ final response = await dio.post(
+ url,
+ data: {
+ "order_items": orderItems.map((item) => item.toMap()).toList(),
+ 'notes': '',
+ },
+ options: Options(
+ headers: {
+ 'Authorization': 'Bearer ${authData.token}',
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ ),
+ );
+
+ if (response.statusCode == 200) {
+ return Right(true);
+ } else {
+ return const Left('Gagal menambahkan pesanan pesanan');
+ }
+ } on DioException catch (e) {
+ final errorMessage =
+ e.response?.data['message'] ?? 'Terjadi kesalahan, coba lagi nanti.';
+ log("💥 Dio error: ${e.message}");
+ log("💥 Dio response: ${e.response?.data}");
+ return Left(errorMessage);
+ } catch (e) {
+ log("💥 Unexpected error: $e");
+ return const Left('Terjadi kesalahan, coba lagi nanti.');
+ }
+ }
+
+ Future> refund({
+ required String orderId,
+ required String reason,
+ required List orderItems,
+ }) async {
+ final authData = await AuthLocalDataSource().getAuthData();
+ final url = '${Variables.baseUrl}/api/v1/orders/$orderId/refund';
+
+ final int refundAmount = orderItems.fold(
+ 0,
+ (sum, item) => sum + ((item.unitPrice ?? 0) * (item.quantity ?? 0)),
+ );
+ try {
+ final response = await dio.post(
+ url,
+ data: {
+ 'refund_amount': refundAmount,
+ "order_items": orderItems
+ .map((item) => {
+ 'order_item_id': item.id,
+ "refund_quantity": item.quantity,
+ "refund_amount": item.totalPrice,
+ "reason": ""
+ })
+ .toList(),
+ 'reason': reason,
+ },
+ options: Options(
+ headers: {
+ 'Authorization': 'Bearer ${authData.token}',
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ ),
+ );
+
+ if (response.statusCode == 200) {
+ return Right(true);
+ } else {
+ return const Left('Gagal refund');
+ }
+ } on DioException catch (e) {
+ final errorMessage =
+ e.response?.data['message'] ?? 'Terjadi kesalahan, coba lagi nanti.';
+ log("💥 Dio error: ${e.message}");
+ log("💥 Dio response: ${e.response?.data}");
+ return Left(errorMessage);
+ } catch (e) {
+ log("💥 Unexpected error: $e");
+ return const Left('Terjadi kesalahan tak terduga');
+ }
+ }
+
+ Future> voidOrder({
+ required String orderId,
+ required String reason,
+ String type = "ITEM", // TYPE: ALL, ITEM
+ required List orderItems,
+ }) async {
+ final authData = await AuthLocalDataSource().getAuthData();
+ final url = '${Variables.baseUrl}/api/v1/orders/void';
+
+ try {
+ final response = await dio.post(
+ url,
+ data: {
+ 'order_id': orderId,
+ 'type': orderItems.isEmpty ? "ALL" : type,
+ 'reason': reason,
+ "items": orderItems
+ .map((item) => {
+ 'order_item_id': item.id,
+ "quantity": item.quantity,
+ })
+ .toList(),
+ },
+ options: Options(
+ headers: {
+ 'Authorization': 'Bearer ${authData.token}',
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ ),
+ );
+
+ if (response.statusCode == 200) {
+ return Right(true);
+ } else {
+ return const Left('Gagal refund');
+ }
+ } on DioException catch (e) {
+ final errorMessage =
+ e.response?.data['message'] ?? 'Terjadi kesalahan, coba lagi nanti.';
+ log("💥 Dio error: ${e.message}");
+ log("💥 Dio response: ${e.response?.data}");
+ return Left(errorMessage);
+ } catch (e) {
+ log("💥 Unexpected error: $e");
+ return const Left('Terjadi kesalahan tak terduga');
+ }
+ }
+
+ Future> refundPayment({
+ required String orderId,
+ required String reason,
+ required int refundAmount,
+ }) async {
+ final authData = await AuthLocalDataSource().getAuthData();
+ final url = '${Variables.baseUrl}/api/v1/orders/$orderId/refund';
+
+ try {
+ final response = await dio.post(
+ url,
+ data: {
+ 'refund_amount': refundAmount,
+ 'reason': reason,
+ },
+ options: Options(
+ headers: {
+ 'Authorization': 'Bearer ${authData.token}',
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ ),
+ );
+
+ if (response.statusCode == 200) {
+ return Right(true);
+ } else {
+ return const Left('Gagal refund');
+ }
+ } on DioException catch (e) {
+ final errorMessage = 'Terjadi kesalahan coba lagi nanti';
+ log("💥 Dio error: ${e.message}");
+ log("💥 Dio response: ${e.response?.data}");
+ return Left(errorMessage);
+ } catch (e) {
+ log("💥 Unexpected error: $e");
+ return const Left('Terjadi kesalahan tak terduga');
+ }
+ }
+
+ Future> createPaymentSplitBill(
+ PaymentSplitBillRequest request) async {
+ final authData = await AuthLocalDataSource().getAuthData();
+ final url = '${Variables.baseUrl}/api/v1/orders/split-bill';
+
+ try {
+ final response = await dio.post(
+ url,
+ data: request.toMap(),
+ options: Options(
+ headers: {
+ 'Authorization': 'Bearer ${authData.token}',
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ ),
+ );
+
+ if (response.statusCode == 200) {
+ return Right(PaymentSuccessResponseModel.fromMap(response.data));
+ } else {
+ return const Left('Gagal membuat pembayaran');
+ }
+ } on DioException catch (e) {
+ final errorMessage =
+ e.response?.data['message'] ?? 'Terjadi kesalahan, coba lagi nanti.';
+ log("💥 Dio error: ${e.message}");
+ log("💥 Dio response: ${e.response?.data}");
+ return Left(errorMessage);
+ } catch (e) {
+ log("💥 Unexpected error: $e");
+ return const Left('Terjadi kesalahan tak terduga');
+ }
+ }
}
diff --git a/lib/data/datasources/outlet_local_datasource.dart b/lib/data/datasources/outlet_local_datasource.dart
new file mode 100644
index 0000000..fa1b6f1
--- /dev/null
+++ b/lib/data/datasources/outlet_local_datasource.dart
@@ -0,0 +1,30 @@
+import 'dart:developer';
+
+import 'package:enaklo_pos/presentation/home/models/outlet_model.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+class OutletLocalDatasource {
+ Future save(Outlet outlet) async {
+ try {
+ final prefs = await SharedPreferences.getInstance();
+ await prefs.setString('outlet', outlet.toJson());
+ log('Outlet Local Data: ${outlet.toJson()}');
+ } catch (e) {
+ log('Error saving outlet: $e');
+ }
+ }
+
+ Future remove() async {
+ final prefs = await SharedPreferences.getInstance();
+ await prefs.remove('outlet');
+ }
+
+ Future get() async {
+ final prefs = await SharedPreferences.getInstance();
+ final outlet = prefs.getString('outlet');
+
+ log('Outlet Local Data: $outlet');
+
+ return Outlet.fromJson(outlet!);
+ }
+}
diff --git a/lib/data/datasources/outlet_remote_data_source.dart b/lib/data/datasources/outlet_remote_data_source.dart
new file mode 100644
index 0000000..c1a66b0
--- /dev/null
+++ b/lib/data/datasources/outlet_remote_data_source.dart
@@ -0,0 +1,96 @@
+import 'dart:developer';
+import 'package:dartz/dartz.dart';
+import 'package:dio/dio.dart';
+import 'package:enaklo_pos/core/network/dio_client.dart';
+import 'package:enaklo_pos/data/datasources/outlet_local_datasource.dart';
+import 'package:enaklo_pos/data/datasources/settings_local_datasource.dart';
+import 'package:enaklo_pos/presentation/home/models/outlet_model.dart';
+import 'package:enaklo_pos/presentation/setting/models/tax_model.dart';
+import '../../core/constants/variables.dart';
+import 'auth_local_datasource.dart';
+
+class OutletRemoteDataSource {
+ final Dio dio = DioClient.instance;
+
+ Future> getOutlets() async {
+ try {
+ final authData = await AuthLocalDataSource().getAuthData();
+
+ final url = '${Variables.baseUrl}/api/v1/outlets/list';
+
+ final response = await dio.get(
+ url,
+ queryParameters: {
+ 'organization_id': authData.user?.organizationId,
+ },
+ options: Options(
+ headers: {
+ 'Authorization': 'Bearer ${authData.token}',
+ 'Accept': 'application/json',
+ },
+ ),
+ );
+
+ if (response.statusCode == 200) {
+ final data = OutletResponse.fromMap(response.data);
+ return Right(data);
+ } else {
+ return const Left('Failed to get outlets');
+ }
+ } on DioException catch (e) {
+ log("Dio error: ${e.message}");
+ return Left(e.response?.data['message'] ?? 'Gagal mengambil outlet');
+ } catch (e) {
+ log("Unexpected error: $e");
+ return const Left('Unexpected error occurred');
+ }
+ }
+
+ Future> currentOutlet() async {
+ try {
+ final authData = await AuthLocalDataSource().getAuthData();
+
+ if (authData.user?.outletId == null) {
+ return const Left('Kamu belum memiliki bergabung dengan outlet');
+ }
+
+ final url =
+ '${Variables.baseUrl}/api/v1/outlets/detail/${authData.user?.outletId}';
+
+ final response = await dio.get(
+ url,
+ queryParameters: {
+ 'organization_id': authData.user?.organizationId,
+ },
+ options: Options(
+ headers: {
+ 'Authorization': 'Bearer ${authData.token}',
+ 'Accept': 'application/json',
+ },
+ ),
+ );
+
+ if (response.statusCode == 200) {
+ final data = OutletDetailResponse.fromMap(response.data);
+ await SettingsLocalDatasource().saveTax(
+ TaxModel(
+ name: 'PB1',
+ type: TaxType.pajak,
+ value: data.data?.taxRate ?? 0,
+ ),
+ );
+
+ await OutletLocalDatasource().save(data.data!);
+ return Right(data);
+ } else {
+ return const Left('Failed to get outlets');
+ }
+ } on DioException catch (e) {
+ log("Dio error: ${e.message}");
+ return Left(e.response?.data['message'] ?? 'Gagal mengambil outlet');
+ } catch (e) {
+ log("Unexpected error: $e");
+ return const Left('Unexpected error occurred');
+ }
+ }
+}
diff --git a/lib/data/datasources/payment_methods_remote_datasource.dart b/lib/data/datasources/payment_methods_remote_datasource.dart
index 4ca105e..e602681 100644
--- a/lib/data/datasources/payment_methods_remote_datasource.dart
+++ b/lib/data/datasources/payment_methods_remote_datasource.dart
@@ -1,34 +1,40 @@
import 'dart:developer';
import 'package:dartz/dartz.dart';
+import 'package:dio/dio.dart';
import 'package:enaklo_pos/core/constants/variables.dart';
+import 'package:enaklo_pos/core/network/dio_client.dart';
import 'package:enaklo_pos/data/datasources/auth_local_datasource.dart';
import 'package:enaklo_pos/data/models/response/payment_methods_response_model.dart';
-import 'package:http/http.dart' as http;
class PaymentMethodsRemoteDatasource {
- Future> getPaymentMethods() async {
+ final Dio dio = DioClient.instance;
+ Future>
+ getPaymentMethods() async {
try {
final authData = await AuthLocalDataSource().getAuthData();
- final response = await http.get(
- Uri.parse('${Variables.baseUrl}/api/payment-methods'),
- headers: {
- 'Authorization': 'Bearer ${authData.token}',
- 'Accept': 'application/json',
- },
+ final response = await dio.get(
+ '${Variables.baseUrl}/api/v1/payment-methods',
+ options: Options(
+ headers: {
+ 'Authorization': 'Bearer ${authData.token}',
+ 'Accept': 'application/json',
+ },
+ ),
);
-
- log("Payment Methods Response Status: ${response.statusCode}");
- log("Payment Methods Response Body: ${response.body}");
-
+
if (response.statusCode == 200) {
- return Right(PaymentMethodsResponseModel.fromJson(response.body));
+ return Right(PaymentMethodsResponseModel.fromMap(response.data));
} else {
return const Left('Failed to get payment methods');
}
+ } on DioException catch (e) {
+ log("Dio error: ${e.message}");
+ return Left(
+ e.response?.data['message'] ?? 'Failed to get payment methods');
} catch (e) {
log("Error getting payment methods: $e");
- return Left('Error: $e');
+ return Left('Unexpected error');
}
}
-}
\ No newline at end of file
+}
diff --git a/lib/data/datasources/product_local_datasource.dart b/lib/data/datasources/product_local_datasource.dart
index a7b63e9..6610f6e 100644
--- a/lib/data/datasources/product_local_datasource.dart
+++ b/lib/data/datasources/product_local_datasource.dart
@@ -7,7 +7,6 @@ import 'package:enaklo_pos/data/models/response/table_model.dart';
import 'package:enaklo_pos/presentation/home/models/order_model.dart';
import 'package:enaklo_pos/presentation/table/models/draft_order_item.dart';
import 'package:enaklo_pos/presentation/table/models/draft_order_model.dart';
-import 'package:intl/intl.dart';
import 'package:sqflite/sqflite.dart';
import '../../presentation/home/models/product_quantity.dart';
@@ -145,31 +144,32 @@ class ProductLocalDatasource {
Future _initDB(String filePath) async {
final dbPath = await getDatabasesPath();
final path = dbPath + filePath;
-
+
// Force delete existing database to ensure new schema
try {
final dbExists = await databaseExists(path);
if (dbExists) {
log("Deleting existing database to ensure new schema with order_type column");
- await deleteDatabase(path);
+ // await deleteDatabase(path);
}
} catch (e) {
log("Error deleting database: $e");
}
-
+
return await openDatabase(
- path,
- version: 2,
+ path,
+ version: 2,
onCreate: _createDb,
onUpgrade: _onUpgrade,
);
}
-
+
Future _onUpgrade(Database db, int oldVersion, int newVersion) async {
if (oldVersion < 2) {
// Add order_type column to orders table if it doesn't exist
try {
- await db.execute('ALTER TABLE $tableOrder ADD COLUMN order_type TEXT DEFAULT "DINE IN"');
+ await db.execute(
+ 'ALTER TABLE $tableOrder ADD COLUMN order_type TEXT DEFAULT "DINE IN"');
log("Added order_type column to orders table");
} catch (e) {
log("order_type column might already exist: $e");
@@ -186,11 +186,11 @@ class ProductLocalDatasource {
//save order
Future saveOrder(OrderModel order) async {
final db = await instance.database;
-
+
// Since we're forcing database recreation, order_type column should exist
final orderMap = order.toMap(includeOrderType: true);
log("Final orderMap for insertion: $orderMap");
-
+
int id = await db.insert(tableOrder, orderMap,
conflictAlgorithm: ConflictAlgorithm.replace);
@@ -238,6 +238,31 @@ class ProductLocalDatasource {
});
}
+ Future> getAllOrderByRange(
+ DateTime start, DateTime end) async {
+ final db = await instance.database;
+
+ // Format ke ISO 8601 untuk range, hasil: yyyy-MM-ddTHH:mm:ss
+ final startIso = start.toIso8601String();
+ final endIso = end.toIso8601String();
+
+ final startDateYYYYMMDD = startIso.substring(0, 10);
+ final endDateYYYYMMDD = endIso.substring(0, 10);
+
+ final List