Merge pull request 'dev' (#1) from dev into main

Reviewed-on: #1
This commit is contained in:
aefril 2025-08-13 17:19:46 +00:00
commit 1d52f22f5f
345 changed files with 53154 additions and 7212 deletions

View File

@ -1,4 +1,4 @@
# EnakloPOS # ApskelPOS
A new Flutter project. A new Flutter project.

View File

@ -23,7 +23,7 @@ if (flutterVersionName == null) {
} }
android { android {
namespace "com.example.enaklo_pos" namespace "com.appscale.pos"
compileSdkVersion 35 compileSdkVersion 35
ndkVersion flutter.ndkVersion ndkVersion flutter.ndkVersion
@ -42,7 +42,7 @@ android {
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). // 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. // 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. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion 21 minSdkVersion 21

View File

@ -11,7 +11,7 @@
<!-- Izin khusus untuk akses foto (media images) di Android 33 ke atas --> <!-- Izin khusus untuk akses foto (media images) di Android 33 ke atas -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<application <application
android:label="EnakloPOS" android:label="ApskelPOS"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/launcher_icon"> android:icon="@mipmap/launcher_icon">
<activity <activity

View File

@ -1,4 +1,4 @@
package com.example.enaklo_pos package com.appscale.pos
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 57 KiB

View File

@ -1,5 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/> <background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/> <foreground>
<inset
android:drawable="@drawable/ic_launcher_foreground"
android:inset="16%" />
</foreground>
</adaptive-icon> </adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="ic_launcher_background">#FFFFFF</color> <color name="ic_launcher_background">#ffffff</color>
</resources> </resources>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

10
assets/icons/people.svg Normal file
View File

@ -0,0 +1,10 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_17_4760)">
<path d="M9 13.75C6.66 13.75 2 14.92 2 17.25V19H16V17.25C16 14.92 11.34 13.75 9 13.75ZM4.34 17C5.18 16.42 7.21 15.75 9 15.75C10.79 15.75 12.82 16.42 13.66 17H4.34ZM9 12C10.93 12 12.5 10.43 12.5 8.5C12.5 6.57 10.93 5 9 5C7.07 5 5.5 6.57 5.5 8.5C5.5 10.43 7.07 12 9 12ZM9 7C9.83 7 10.5 7.67 10.5 8.5C10.5 9.33 9.83 10 9 10C8.17 10 7.5 9.33 7.5 8.5C7.5 7.67 8.17 7 9 7ZM16.04 13.81C17.2 14.65 18 15.77 18 17.25V19H22V17.25C22 15.23 18.5 14.08 16.04 13.81ZM15 12C16.93 12 18.5 10.43 18.5 8.5C18.5 6.57 16.93 5 15 5C14.46 5 13.96 5.13 13.5 5.35C14.13 6.24 14.5 7.33 14.5 8.5C14.5 9.67 14.13 10.76 13.5 11.65C13.96 11.87 14.46 12 15 12Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_17_4760">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 892 B

BIN
assets/images/gojek.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

BIN
assets/images/grab.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 79 KiB

View File

@ -4,31 +4,30 @@ import 'lib/core/utils/app_icon_generator.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
print('Generating EnakloPOS app icon...'); print('Generating ApskelPOS app icon...');
try { try {
final iconData = await AppIconGenerator.generateAppIcon(); final iconData = await AppIconGenerator.generateAppIcon();
// Ensure the assets/logo directory exists // Ensure the assets/logo directory exists
final logoDir = Directory('assets/logo'); final logoDir = Directory('assets/logo');
if (!await logoDir.exists()) { if (!await logoDir.exists()) {
await logoDir.create(recursive: true); await logoDir.create(recursive: true);
} }
// Write the generated icon to file // 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); 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('📱 The icon features:');
print(' - White background for visibility'); print(' - White background for visibility');
print(' - Blue circular background'); print(' - Blue circular background');
print(' - Gift box with "e" inside'); print(' - Gift box with "e" inside');
print(' - "ENAKLO" and "POS" text'); print(' - "ENAKLO" and "POS" text');
print(' - 1024x1024 resolution for high quality'); print(' - 1024x1024 resolution for high quality');
} catch (e) { } catch (e) {
print('❌ Error generating app icon: $e'); print('❌ Error generating app icon: $e');
} }
} }

View File

@ -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":"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"}}
"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"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 664 B

After

Width:  |  Height:  |  Size: 666 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 984 B

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -5,7 +5,7 @@
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>EnakloPOS</string> <string>ApskelPOS</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>

17
launcher_icon.yaml Normal file
View File

@ -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

View File

@ -101,6 +101,9 @@ class $AssetsIconsGen {
/// File path: assets/icons/payments.svg /// File path: assets/icons/payments.svg
SvgGenImage get payments => const SvgGenImage('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 /// File path: assets/icons/print.svg
SvgGenImage get print => const SvgGenImage('assets/icons/print.svg'); SvgGenImage get print => const SvgGenImage('assets/icons/print.svg');
@ -152,6 +155,7 @@ class $AssetsIconsGen {
orders, orders,
pajak, pajak,
payments, payments,
people,
print, print,
qrCode, qrCode,
report, report,
@ -186,6 +190,12 @@ class $AssetsImagesGen {
/// File path: assets/images/drink7.png /// File path: assets/images/drink7.png
AssetGenImage get drink7 => const AssetGenImage('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 /// File path: assets/images/logo.png
AssetGenImage get logo => const AssetGenImage('assets/images/logo.png'); AssetGenImage get logo => const AssetGenImage('assets/images/logo.png');
@ -265,6 +275,8 @@ class $AssetsImagesGen {
drink5, drink5,
drink6, drink6,
drink7, drink7,
gojek,
grab,
logo, logo,
managePrinter, managePrinter,
manageProduct, manageProduct,

View File

@ -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';
}

View File

@ -18,6 +18,10 @@ class Button extends StatelessWidget {
this.icon, this.icon,
this.disabled = false, this.disabled = false,
this.fontSize = 16.0, this.fontSize = 16.0,
this.elevation,
this.labelStyle,
this.mainAxisAlignment = MainAxisAlignment.center,
this.crossAxisAlignment = CrossAxisAlignment.center,
}); });
const Button.outlined({ const Button.outlined({
@ -33,9 +37,13 @@ class Button extends StatelessWidget {
this.icon, this.icon,
this.disabled = false, this.disabled = false,
this.fontSize = 16.0, 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 String label;
final ButtonStyle style; final ButtonStyle style;
final Color color; final Color color;
@ -43,9 +51,13 @@ class Button extends StatelessWidget {
final double? width; final double? width;
final double height; final double height;
final double borderRadius; final double borderRadius;
final double? elevation;
final Widget? icon; final Widget? icon;
final bool disabled; final bool disabled;
final double fontSize; final double fontSize;
final TextStyle? labelStyle;
final MainAxisAlignment mainAxisAlignment;
final CrossAxisAlignment crossAxisAlignment;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -60,11 +72,12 @@ class Button extends StatelessWidget {
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(borderRadius), borderRadius: BorderRadius.circular(borderRadius),
), ),
elevation: elevation,
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: const EdgeInsets.symmetric(horizontal: 16.0),
), ),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: mainAxisAlignment,
mainAxisSize: MainAxisSize.min, crossAxisAlignment: crossAxisAlignment,
children: [ children: [
icon ?? const SizedBox.shrink(), icon ?? const SizedBox.shrink(),
if (icon != null) const SizedBox(width: 10.0), if (icon != null) const SizedBox(width: 10.0),
@ -73,11 +86,12 @@ class Button extends StatelessWidget {
fit: BoxFit.scaleDown, fit: BoxFit.scaleDown,
child: Text( child: Text(
label, label,
style: TextStyle( style: labelStyle ??
color: disabled ? Colors.grey : textColor, TextStyle(
fontSize: fontSize, color: disabled ? Colors.grey : textColor,
fontWeight: FontWeight.w600, fontSize: fontSize,
), fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
), ),
@ -89,14 +103,15 @@ class Button extends StatelessWidget {
onPressed: disabled ? null : onPressed, onPressed: disabled ? null : onPressed,
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
backgroundColor: color, backgroundColor: color,
side: const BorderSide(color: Colors.grey), side: const BorderSide(color: AppColors.primary),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(borderRadius), borderRadius: BorderRadius.circular(borderRadius),
), ),
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: const EdgeInsets.symmetric(horizontal: 16.0),
), ),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: mainAxisAlignment,
crossAxisAlignment: crossAxisAlignment,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
icon ?? const SizedBox.shrink(), icon ?? const SizedBox.shrink(),
@ -106,11 +121,12 @@ class Button extends StatelessWidget {
fit: BoxFit.scaleDown, fit: BoxFit.scaleDown,
child: Text( child: Text(
label, label,
style: TextStyle( style: labelStyle ??
color: disabled ? Colors.grey : textColor, TextStyle(
fontSize: fontSize, color: disabled ? Colors.grey : textColor,
fontWeight: FontWeight.w600, fontSize: fontSize,
), fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
), ),

View File

@ -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,
),
),
],
),
),
),
);
}
}

View File

@ -14,6 +14,8 @@ class CustomTextField extends StatelessWidget {
final Widget? prefixIcon; final Widget? prefixIcon;
final Widget? suffixIcon; final Widget? suffixIcon;
final bool readOnly; final bool readOnly;
final int maxLines;
final String? Function(String?)? validator;
const CustomTextField({ const CustomTextField({
super.key, super.key,
@ -28,6 +30,8 @@ class CustomTextField extends StatelessWidget {
this.prefixIcon, this.prefixIcon,
this.suffixIcon, this.suffixIcon,
this.readOnly = false, this.readOnly = false,
this.maxLines = 1,
this.validator,
}); });
@override @override
@ -53,17 +57,11 @@ class CustomTextField extends StatelessWidget {
textInputAction: textInputAction, textInputAction: textInputAction,
textCapitalization: textCapitalization ?? TextCapitalization.none, textCapitalization: textCapitalization ?? TextCapitalization.none,
readOnly: readOnly, readOnly: readOnly,
maxLines: maxLines,
validator: validator,
decoration: InputDecoration( decoration: InputDecoration(
prefixIcon: prefixIcon, prefixIcon: prefixIcon,
suffixIcon: suffixIcon, 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, hintText: label,
), ),
), ),

View File

@ -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),
),
);
}),
);
},
),
);
}
}

View File

@ -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);
}
}

View File

@ -1,91 +1,189 @@
import 'dart:io'; import 'dart:io';
import 'package:enaklo_pos/presentation/setting/bloc/upload_file/upload_file_bloc.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import '../assets/assets.gen.dart';
import '../constants/colors.dart'; import '../constants/colors.dart';
import '../constants/variables.dart'; import '../constants/variables.dart';
import 'buttons.dart';
import 'spaces.dart'; import 'spaces.dart';
class ImagePickerWidget extends StatefulWidget { class ImagePickerWidget extends StatefulWidget {
final String label; final String label;
final void Function(XFile? file) onChanged; final void Function(XFile? file) onChanged;
final void Function(String? uploadedUrl)? onUploaded;
final bool showLabel; final bool showLabel;
final String? initialImageUrl; final String? initialImageUrl;
final bool autoUpload;
const ImagePickerWidget({ const ImagePickerWidget({
super.key, super.key,
required this.label, required this.label,
required this.onChanged, required this.onChanged,
this.onUploaded,
this.showLabel = true, this.showLabel = true,
this.initialImageUrl, this.initialImageUrl,
this.autoUpload = false,
}); });
@override @override
State<ImagePickerWidget> createState() => _ImagePickerWidgetState(); State<ImagePickerWidget> createState() => _ImagePickerWidgetState();
} }
class _ImagePickerWidgetState extends State<ImagePickerWidget> { class _ImagePickerWidgetState extends State<ImagePickerWidget>
with TickerProviderStateMixin {
String? imagePath; String? imagePath;
String? uploadedImageUrl;
bool hasInitialImage = false; bool hasInitialImage = false;
bool isHovering = false;
bool isUploading = false;
late AnimationController _scaleController;
late AnimationController _fadeController;
late AnimationController _uploadController;
late Animation<double> _scaleAnimation;
late Animation<double> _fadeAnimation;
late Animation<double> _uploadAnimation;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
hasInitialImage = widget.initialImageUrl != null; 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<double>(
begin: 1.0,
end: 0.95,
).animate(CurvedAnimation(
parent: _scaleController,
curve: Curves.easeInOut,
));
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _fadeController,
curve: Curves.easeInOut,
));
_uploadAnimation = Tween<double>(
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<void> _pickImage() async { Future<void> _pickImage() async {
_scaleController.forward().then((_) {
_scaleController.reverse();
});
final pickedFile = await ImagePicker().pickImage( final pickedFile = await ImagePicker().pickImage(
source: ImageSource.gallery, source: ImageSource.gallery,
); );
setState(() { if (pickedFile != null) {
if (pickedFile != null) { setState(() {
imagePath = pickedFile.path; imagePath = pickedFile.path;
hasInitialImage = false; // Clear initial image when new image is picked hasInitialImage = false;
widget.onChanged(pickedFile); uploadedImageUrl = null;
} else { });
debugPrint('No image selected.');
widget.onChanged(null); widget.onChanged(pickedFile);
// Auto upload if enabled
if (widget.autoUpload) {
_uploadImage(pickedFile.path);
} }
}); } else {
debugPrint('No image selected.');
widget.onChanged(null);
}
} }
@override void _uploadImage(String filePath) {
Widget build(BuildContext context) { setState(() {
return Column( isUploading = true;
crossAxisAlignment: CrossAxisAlignment.start, });
children: [ _uploadController.forward();
if (widget.showLabel) ...[
Text( context.read<UploadFileBloc>().add(
widget.label, UploadFileEvent.upload(filePath),
style: const TextStyle( );
fontSize: 14, }
fontWeight: FontWeight.w700,
), 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), child: ClipRRect(
decoration: BoxDecoration( borderRadius: BorderRadius.circular(20.0),
borderRadius: BorderRadius.circular(16.0), child: Stack(
border: Border.all(color: AppColors.primary), children: [
), Positioned.fill(
child: Row( child: imagePath != null
children: [ ? Image.file(
SizedBox( File(imagePath!),
width: 80.0, fit: BoxFit.cover,
height: 80.0, )
child: ClipRRect( : uploadedImageUrl != null
borderRadius: BorderRadius.circular(10.0), ? CachedNetworkImage(
child: imagePath != null imageUrl: uploadedImageUrl!.contains('http')
? Image.file( ? uploadedImageUrl!
File(imagePath!), : '${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, fit: BoxFit.cover,
) )
: hasInitialImage && widget.initialImageUrl != null : hasInitialImage && widget.initialImageUrl != null
@ -93,38 +191,493 @@ class _ImagePickerWidgetState extends State<ImagePickerWidget> {
imageUrl: widget.initialImageUrl!.contains('http') imageUrl: widget.initialImageUrl!.contains('http')
? widget.initialImageUrl! ? widget.initialImageUrl!
: '${Variables.baseUrl}/${widget.initialImageUrl}', : '${Variables.baseUrl}/${widget.initialImageUrl}',
placeholder: (context, url) => placeholder: (context, url) => Container(
const Center(child: CircularProgressIndicator()), decoration: BoxDecoration(
errorWidget: (context, url, error) => Container( gradient: LinearGradient(
padding: const EdgeInsets.all(16.0), colors: [
color: AppColors.black.withOpacity(0.05), AppColors.primary.withOpacity(0.1),
child: Assets.icons.image.svg(), 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, fit: BoxFit.cover,
) )
: Container( : _buildPlaceholder(),
padding: const EdgeInsets.all(16.0), ),
color: AppColors.black.withOpacity(0.05), // Upload progress overlay
child: Assets.icons.image.svg(), 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(), // Overlay gradient for better button visibility
Padding( if ((imagePath != null ||
padding: const EdgeInsets.only(right: 10.0), uploadedImageUrl != null ||
child: Button.filled( (hasInitialImage && widget.initialImageUrl != null)) &&
height: 30.0, !isUploading)
width: 140.0, Positioned.fill(
onPressed: _pickImage, child: Container(
label: 'Choose Photo', decoration: BoxDecoration(
fontSize: 12.0, gradient: LinearGradient(
borderRadius: 5.0, 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<UploadFileBloc, UploadFileState>(
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<FileRemoteDataSource>(),
/// ),
/// child: ImagePickerWidget(...),
/// )
/// ```

View File

@ -2,8 +2,6 @@ import 'package:flutter/material.dart';
import '../constants/colors.dart'; import '../constants/colors.dart';
class SearchInput extends StatelessWidget { class SearchInput extends StatelessWidget {
final TextEditingController controller; final TextEditingController controller;
final Function(String value)? onChanged; final Function(String value)? onChanged;

View File

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
class AppColors { class AppColors {
/// primary = #3949AB /// primary = #3949AB
static const Color primary = Color(0xff6466f1); static const Color primary = Color(0xff36175e);
/// grey = #B7B7B7 /// grey = #B7B7B7
static const Color grey = Color(0xffB7B7B7); static const Color grey = Color(0xffB7B7B7);
@ -18,6 +18,7 @@ class AppColors {
/// white = #FFFFFF /// white = #FFFFFF
static const Color white = Color(0xffFFFFFF); static const Color white = Color(0xffFFFFFF);
static const Color whiteText = Color(0xfff1eaf9);
/// green = #50C474 /// green = #50C474
static const Color green = Color(0xff50C474); static const Color green = Color(0xff50C474);
@ -36,4 +37,10 @@ class AppColors {
/// stroke = #EFF0F6 /// stroke = #EFF0F6
static const Color stroke = Color(0xffEFF0F6); 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);
} }

View File

@ -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),
),
),
);

View File

@ -2,5 +2,6 @@ class Variables {
static const String appName = 'POS Kasir Resto App'; static const String appName = 'POS Kasir Resto App';
static const String apiVersion = 'v1'; static const String apiVersion = 'v1';
// static const String baseUrl = 'http://192.168.1.202:8000'; // 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;
} }

View File

@ -15,4 +15,12 @@ extension StringExt on String {
decimalDigits: 0, decimalDigits: 0,
).format(parsedValue); ).format(parsedValue);
} }
String toTitleCase() {
if (isEmpty) return '';
return split(' ').map((word) {
if (word.isEmpty) return '';
return word[0].toUpperCase() + word.substring(1).toLowerCase();
}).join(' ');
}
} }

View File

@ -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<void> onPrint(
BuildContext context, {
required List<ProductQuantity> 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<void> 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<Uint8List> 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));
}

View File

@ -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<NavigatorState> navigatorKey =
GlobalKey<NavigatorState>();
@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<void> _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<NavigatorState> navigatorKey =
GlobalKey<NavigatorState>();
@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<bool> _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<Response> _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<void> _handleTokenExpired() async {
final prefs = await SharedPreferences.getInstance();
await prefs.clear();
final context = navigatorKey.currentContext;
if (context != null) {
Navigator.of(context).pushNamedAndRemoveUntil(
'/login',
(route) => false,
);
}
}
}

View File

@ -38,7 +38,7 @@ class ItemSalesInvoice {
return HelperPdfService.saveDocument( return HelperPdfService.saveDocument(
name: name:
'Enaklo POS | Item Sales Report | ${DateTime.now().millisecondsSinceEpoch}.pdf', 'Apskel POS | Item Sales Report | ${DateTime.now().millisecondsSinceEpoch}.pdf',
pdf: pdf); pdf: pdf);
} }
@ -48,7 +48,7 @@ class ItemSalesInvoice {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SizedBox(height: 1 * PdfPageFormat.cm), SizedBox(height: 1 * PdfPageFormat.cm),
Text('Enaklo POS | Item Sales Report', Text('Apskel POS | Item Sales Report',
style: TextStyle( style: TextStyle(
fontSize: 20, fontSize: 20,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,

View File

@ -22,13 +22,14 @@ class RevenueInvoice {
log("Starting PDF generation for summary report"); log("Starting PDF generation for summary report");
log("Summary model: ${summaryModel.toMap()}"); log("Summary model: ${summaryModel.toMap()}");
log("Search date formatted: $searchDateFormatted"); log("Search date formatted: $searchDateFormatted");
final pdf = Document(); final pdf = Document();
log("PDF document created"); log("PDF document created");
// Load logo image // Load logo image
log("Loading 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 Uint8List bytes = dataImage.buffer.asUint8List();
final image = pw.MemoryImage(bytes); final image = pw.MemoryImage(bytes);
log("Logo image loaded successfully, size: ${bytes.length} bytes"); log("Logo image loaded successfully, size: ${bytes.length} bytes");
@ -49,7 +50,7 @@ class RevenueInvoice {
log("Saving PDF document..."); log("Saving PDF document...");
return HelperPdfService.saveDocument( return HelperPdfService.saveDocument(
name: name:
'Enaklo POS | Summary Sales Report | ${DateTime.now().millisecondsSinceEpoch}.pdf', 'Apskel POS | Summary Sales Report | ${DateTime.now().millisecondsSinceEpoch}.pdf',
pdf: pdf, pdf: pdf,
); );
} catch (e) { } catch (e) {
@ -69,7 +70,7 @@ class RevenueInvoice {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SizedBox(height: 1 * PdfPageFormat.cm), SizedBox(height: 1 * PdfPageFormat.cm),
Text('Enaklo POS | Summary Sales Report', Text('Apskel POS | Summary Sales Report',
style: TextStyle( style: TextStyle(
fontSize: 20, fontSize: 20,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -93,7 +94,7 @@ class RevenueInvoice {
static Widget buildTotal(SummaryModel summaryModel) { static Widget buildTotal(SummaryModel summaryModel) {
log("Building total section with summary model: ${summaryModel.toMap()}"); log("Building total section with summary model: ${summaryModel.toMap()}");
// Helper function to safely parse string to int // Helper function to safely parse string to int
int safeParseInt(String? value) { int safeParseInt(String? value) {
if (value == null || value.isEmpty) return 0; if (value == null || value.isEmpty) return 0;
@ -104,7 +105,7 @@ class RevenueInvoice {
return 0; return 0;
} }
} }
return Container( return Container(
width: double.infinity, width: double.infinity,
child: Column( child: Column(
@ -125,7 +126,8 @@ class RevenueInvoice {
buildText( buildText(
title: 'Discount', title: 'Discount',
titleStyle: TextStyle(fontWeight: FontWeight.normal), titleStyle: TextStyle(fontWeight: FontWeight.normal),
value: "- ${safeParseInt(summaryModel.totalDiscount).currencyFormatRp}", value:
"- ${safeParseInt(summaryModel.totalDiscount).currencyFormatRp}",
unite: true, unite: true,
textStyle: TextStyle( textStyle: TextStyle(
color: PdfColor.fromHex('#FF0000'), color: PdfColor.fromHex('#FF0000'),
@ -147,7 +149,8 @@ class RevenueInvoice {
titleStyle: TextStyle( titleStyle: TextStyle(
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,
), ),
value: safeParseInt(summaryModel.totalServiceCharge).currencyFormatRp, value:
safeParseInt(summaryModel.totalServiceCharge).currencyFormatRp,
unite: true, unite: true,
), ),
Divider(), Divider(),

View File

@ -5,7 +5,7 @@ import 'package:enaklo_pos/core/extensions/int_ext.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:enaklo_pos/core/utils/helper_pdf_service.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/widgets.dart';
import 'package:pdf/pdf.dart'; import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw; import 'package:pdf/widgets.dart' as pw;
@ -13,7 +13,7 @@ import 'package:pdf/widgets.dart' as pw;
class TransactionSalesInvoice { class TransactionSalesInvoice {
static late Font ttf; static late Font ttf;
static Future<File> generate( static Future<File> generate(
List<ItemOrder> itemOrders, String searchDateFormatted) async { List<Order> itemOrders, String searchDateFormatted) async {
final pdf = Document(); final pdf = Document();
// var data = await rootBundle.load("assets/fonts/noto-sans.ttf"); // var data = await rootBundle.load("assets/fonts/noto-sans.ttf");
// ttf = Font.ttf(data); // ttf = Font.ttf(data);
@ -38,7 +38,7 @@ class TransactionSalesInvoice {
return HelperPdfService.saveDocument( return HelperPdfService.saveDocument(
name: name:
'Enaklo POS | Transaction Sales Report | ${DateTime.now().millisecondsSinceEpoch}.pdf', 'Apskel POS | Transaction Sales Report | ${DateTime.now().millisecondsSinceEpoch}.pdf',
pdf: pdf); pdf: pdf);
} }
@ -48,7 +48,7 @@ class TransactionSalesInvoice {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SizedBox(height: 1 * PdfPageFormat.cm), SizedBox(height: 1 * PdfPageFormat.cm),
Text('Enaklo POS | Transaction Sales Report', Text('Apskel POS | Transaction Sales Report',
style: TextStyle( style: TextStyle(
fontSize: 20, fontSize: 20,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -70,7 +70,7 @@ class TransactionSalesInvoice {
), ),
]); ]);
static Widget buildInvoice(List<ItemOrder> itemOrders) { static Widget buildInvoice(List<Order> itemOrders) {
final headers = [ final headers = [
'Total', 'Total',
'Sub Total', 'Sub Total',
@ -81,12 +81,13 @@ class TransactionSalesInvoice {
]; ];
final data = itemOrders.map((item) { final data = itemOrders.map((item) {
return [ return [
item.total!.currencyFormatRp, item.totalAmount!.currencyFormatRp,
item.subTotal!.currencyFormatRp, item.subtotal!.currencyFormatRp,
item.tax!.currencyFormatRp, item.taxAmount!.currencyFormatRp,
int.parse(item.discountAmount!.replaceAll('.00', '')).currencyFormatRp, int.parse(item.discountAmount!.toString().replaceAll('.00', ''))
item.serviceCharge!.currencyFormatRp, .currencyFormatRp,
item.transactionTime!.toFormattedDate2(), 0,
item.createdAt!.toFormattedDate2(),
]; ];
}).toList(); }).toList();

View File

@ -1,5 +1,7 @@
import 'dart:math'; 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:esc_pos_utils_plus/esc_pos_utils_plus.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:enaklo_pos/core/extensions/int_ext.dart'; import 'package:enaklo_pos/core/extensions/int_ext.dart';
@ -35,7 +37,7 @@ class PrintDataoutputs {
final total = totalPrice + pajak; final total = totalPrice + pajak;
bytes += generator.reset(); bytes += generator.reset();
bytes += generator.text('Enaklo POS', bytes += generator.text('Apskel POS',
styles: const PosStyles( styles: const PosStyles(
bold: true, bold: true,
align: PosAlign.center, align: PosAlign.center,
@ -60,12 +62,12 @@ class PrintDataoutputs {
bytes += generator.row([ bytes += generator.row([
PosColumn( PosColumn(
text: text:
'${product.product.price!.toIntegerFromText.currencyFormatRp} x ${product.quantity}', '${product.product.price!.currencyFormatRp} x ${product.quantity}',
width: 8, width: 8,
styles: const PosStyles(align: PosAlign.left), styles: const PosStyles(align: PosAlign.left),
), ),
PosColumn( PosColumn(
text: '${product.product.price!.toIntegerFromText * product.quantity}' text: '${product.product.price! * product.quantity}'
.toIntegerFromText .toIntegerFromText
.currencyFormatRp, .currencyFormatRp,
width: 4, width: 4,
@ -202,7 +204,7 @@ class PrintDataoutputs {
// bytes += generator.feed(3); // bytes += generator.feed(3);
// } // }
bytes += generator.text('Enaklo POS', bytes += generator.text('Apskel POS',
styles: const PosStyles( styles: const PosStyles(
bold: true, bold: true,
align: PosAlign.center, align: PosAlign.center,
@ -328,8 +330,7 @@ class PrintDataoutputs {
styles: const PosStyles(align: PosAlign.left), styles: const PosStyles(align: PosAlign.left),
), ),
PosColumn( PosColumn(
text: (product.product.price!.toIntegerFromText * product.quantity) text: (product.product.price! * product.quantity).currencyFormatRp,
.currencyFormatRp,
width: 4, width: 4,
styles: const PosStyles(align: PosAlign.right), styles: const PosStyles(align: PosAlign.right),
), ),
@ -398,8 +399,7 @@ class PrintDataoutputs {
styles: const PosStyles(align: PosAlign.left), styles: const PosStyles(align: PosAlign.left),
), ),
PosColumn( PosColumn(
text: (products[0].product.price!.toIntegerFromText * text: (products[0].product.price! * products[0].quantity)
products[0].quantity)
.currencyFormatRp, .currencyFormatRp,
width: 4, width: 4,
styles: const PosStyles(align: PosAlign.right), styles: const PosStyles(align: PosAlign.right),
@ -430,8 +430,7 @@ class PrintDataoutputs {
styles: const PosStyles(align: PosAlign.left), styles: const PosStyles(align: PosAlign.left),
), ),
PosColumn( PosColumn(
text: (products[0].product.price!.toIntegerFromText * text: (products[0].product.price! * products[0].quantity)
products[0].quantity)
.currencyFormatRp, .currencyFormatRp,
width: 4, width: 4,
styles: const PosStyles(align: PosAlign.right), styles: const PosStyles(align: PosAlign.right),
@ -474,21 +473,21 @@ class PrintDataoutputs {
} }
Future<List<int>> printOrderV3( Future<List<int>> printOrderV3(
List<ProductQuantity> products, List<ProductQuantity> products,
int totalQuantity, int totalQuantity,
int totalPrice, int totalPrice,
String paymentMethod, String paymentMethod,
int nominalBayar, int nominalBayar,
int kembalian, int kembalian,
int subTotal, int subTotal,
int discount, int discount,
int pajak, int pajak,
int serviceCharge, int serviceCharge,
String namaKasir, String namaKasir,
String customerName, String customerName,
int paper, int paper,
{int taxPercentage = 11, int serviceChargePercentage = 5} {int taxPercentage = 11,
) async { int serviceChargePercentage = 5}) async {
List<int> bytes = []; List<int> bytes = [];
final profile = await CapabilityProfile.load(); final profile = await CapabilityProfile.load();
@ -610,8 +609,8 @@ class PrintDataoutputs {
styles: const PosStyles(bold: true, align: PosAlign.left), styles: const PosStyles(bold: true, align: PosAlign.left),
), ),
PosColumn( PosColumn(
text: '${product.product.price!.toIntegerFromText * product.quantity}' text:
.currencyFormatRpV2, '${product.product.price! * product.quantity}'.currencyFormatRpV2,
width: 4, width: 4,
styles: const PosStyles(bold: true, align: PosAlign.right), styles: const PosStyles(bold: true, align: PosAlign.right),
), ),
@ -626,8 +625,7 @@ class PrintDataoutputs {
final subTotalPrice = products.fold<int>( final subTotalPrice = products.fold<int>(
0, 0,
(previousValue, element) => (previousValue, element) =>
previousValue + previousValue + (element.product.price! * element.quantity));
(element.product.price!.toIntegerFromText * element.quantity));
bytes += generator.row([ bytes += generator.row([
PosColumn( PosColumn(
text: 'Subtotal $totalQuantity Product', text: 'Subtotal $totalQuantity Product',
@ -743,6 +741,265 @@ class PrintDataoutputs {
return bytes; return bytes;
} }
Future<List<int>> printOrderV4(
Order order,
String chashierName,
String paymentMethod,
int nominalBayar,
int kembalian,
int taxPercentage,
int paper,
String orderType,
Outlet outlet,
) async {
List<int> 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 ?? <OrderItem>[])) {
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<List<int>> printQRIS( Future<List<int>> printQRIS(
int totalPrice, Uint8List imageQris, int paper) async { int totalPrice, Uint8List imageQris, int paper) async {
List<int> bytes = []; List<int> bytes = [];
@ -778,8 +1035,13 @@ class PrintDataoutputs {
return bytes; return bytes;
} }
Future<List<int>> printChecker(List<ProductQuantity> products, Future<List<int>> printChecker(
String tableName, String draftName, String cashierName, int paper, String orderType) async { List<ProductQuantity> products,
String tableName,
String draftName,
String cashierName,
int paper,
String orderType) async {
List<int> bytes = []; List<int> bytes = [];
final profile = await CapabilityProfile.load(); final profile = await CapabilityProfile.load();
@ -908,8 +1170,13 @@ class PrintDataoutputs {
return bytes; return bytes;
} }
Future<List<int>> printKitchen(List<ProductQuantity> products, Future<List<int>> printKitchen(
String tableNumber, String draftName, String cashierName, int paper, String orderType) async { List<ProductQuantity> products,
String tableNumber,
String draftName,
String cashierName,
int paper,
String orderType) async {
List<int> bytes = []; List<int> bytes = [];
final profile = await CapabilityProfile.load(); final profile = await CapabilityProfile.load();
@ -1134,4 +1401,39 @@ class PrintDataoutputs {
return bytes; return bytes;
} }
Future<List<int>> printTicket(
int totalPrice, Uint8List imageQris, int paper) async {
List<int> 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;
}
} }

View File

@ -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<Either<String, PaymentMethodAnalyticResponseModel>> 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<Either<String, SalesAnalyticResponseModel>> 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<Either<String, ProductAnalyticResponseModel>> 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<Either<String, DashboardAnalyticResponseModel>> 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<Either<String, ProfitLossResponseModel>> 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');
}
}
}

View File

@ -1,10 +1,16 @@
import 'dart:developer';
import 'package:enaklo_pos/data/models/response/auth_response_model.dart'; import 'package:enaklo_pos/data/models/response/auth_response_model.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
class AuthLocalDataSource { class AuthLocalDataSource {
Future<void> saveAuthData(AuthResponseModel authResponseModel) async { Future<void> saveAuthData(AuthResponseModel authResponseModel) async {
final prefs = await SharedPreferences.getInstance(); try {
await prefs.setString('auth_data', authResponseModel.toJson()); final prefs = await SharedPreferences.getInstance();
await prefs.setString('auth_data', authResponseModel.toJson());
} catch (e) {
log('Error saving auth data: $e');
}
} }
Future<void> removeAuthData() async { Future<void> removeAuthData() async {
@ -16,6 +22,8 @@ class AuthLocalDataSource {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
final authData = prefs.getString('auth_data'); final authData = prefs.getString('auth_data');
log('Auth data: $authData');
return AuthResponseModel.fromJson(authData!); return AuthResponseModel.fromJson(authData!);
} }

View File

@ -1,44 +1,67 @@
import 'dart:developer';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:dio/dio.dart';
import 'package:enaklo_pos/core/constants/variables.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/datasources/auth_local_datasource.dart';
import 'package:enaklo_pos/data/models/response/auth_response_model.dart'; import 'package:enaklo_pos/data/models/response/auth_response_model.dart';
import 'package:http/http.dart' as http;
class AuthRemoteDatasource { class AuthRemoteDatasource {
final Dio dio = DioClient.instance;
Future<Either<String, AuthResponseModel>> login( Future<Either<String, AuthResponseModel>> login(
String email, String password) async { String email, String password) async {
final url = Uri.parse('${Variables.baseUrl}/api/login'); final url = '${Variables.baseUrl}/api/v1/auth/login';
final response = await http.post( log(url);
url,
body: {
'email': email,
'password': password,
},
);
if (response.statusCode == 200) { try {
return Right(AuthResponseModel.fromJson(response.body)); final response = await dio.post(
} else { url,
return const Left('Failed to login'); 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 //logout
Future<Either<String, bool>> logout() async { Future<Either<String, bool>> logout() async {
final authData = await AuthLocalDataSource().getAuthData(); try {
final url = Uri.parse('${Variables.baseUrl}/api/logout'); final authData = await AuthLocalDataSource().getAuthData();
final response = await http.post( final url = '${Variables.baseUrl}/api/v1/auth/logout';
url,
headers: {
'Authorization': 'Bearer ${authData.token}',
'Accept': 'application/json',
},
);
if (response.statusCode == 200) { final response = await dio.post(
return const Right(true); url,
} else { options: Options(
return const Left('Failed to logout'); 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');
} }
} }
} }

View File

@ -1,27 +1,48 @@
import 'dart:developer'; import 'dart:developer';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:dio/dio.dart';
import 'package:enaklo_pos/core/constants/variables.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/datasources/auth_local_datasource.dart';
import 'package:enaklo_pos/data/models/response/category_response_model.dart'; import 'package:enaklo_pos/data/models/response/category_response_model.dart';
import 'package:http/http.dart' as http;
class CategoryRemoteDatasource { class CategoryRemoteDatasource {
Future<Either<String, CategroyResponseModel>> getCategories() async { final Dio dio = DioClient.instance;
Future<Either<String, CategoryResponseModel>> getCategories({
int page = 1,
int limit = 10,
bool isActive = true,
}) async {
final authData = await AuthLocalDataSource().getAuthData(); final authData = await AuthLocalDataSource().getAuthData();
final Map<String, String> headers = { final headers = {
'Authorization': 'Bearer ${authData.token}', 'Authorization': 'Bearer ${authData.token}',
'Accept': 'application/json', 'Accept': 'application/json',
}; };
final response = await http.get(
Uri.parse('${Variables.baseUrl}/api/api-categories'), try {
headers: headers); final response = await dio.get(
log(response.statusCode.toString()); '${Variables.baseUrl}/api/v1/categories',
log(response.body); queryParameters: {
if (response.statusCode == 200) { 'page': page,
return right(CategroyResponseModel.fromJson(response.body)); 'limit': limit,
} else { 'is_active': isActive,
return left(response.body); },
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');
} }
} }
} }

View File

@ -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<Either<String, CustomerResponseModel>> 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<Either<String, bool>> 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<String, dynamic> 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');
}
}
}

View File

@ -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<DeliveryModel> deliveries = [
DeliveryModel(
id: 'gojek',
name: 'Gojek',
imageUrl: Assets.images.gojek.path,
),
DeliveryModel(
id: 'grab',
name: 'Grab',
imageUrl: Assets.images.grab.path,
),
];

View File

@ -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<Either<String, FileResponseModel>> 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');
}
}
}

View File

@ -1,17 +1,25 @@
import 'dart:convert';
import 'dart:developer'; import 'dart:developer';
import 'dart:convert';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:dio/dio.dart';
import 'package:enaklo_pos/core/constants/variables.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/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_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/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_model.dart';
import 'package:enaklo_pos/presentation/home/models/order_request.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:intl/intl.dart';
class OrderRemoteDatasource { class OrderRemoteDatasource {
final Dio dio = DioClient.instance;
//save order to remote server //save order to remote server
Future<bool> saveOrder(OrderModel orderModel) async { Future<bool> saveOrder(OrderModel orderModel) async {
final authData = await AuthLocalDataSource().getAuthData(); final authData = await AuthLocalDataSource().getAuthData();
@ -28,11 +36,11 @@ class OrderRemoteDatasource {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
); );
print("📥 HTTP Status Code: ${response.statusCode}"); print("📥 HTTP Status Code: ${response.statusCode}");
print("📥 Response Body: ${response.body}"); print("📥 Response Body: ${response.body}");
print("📥 Response Headers: ${response.headers}"); print("📥 Response Headers: ${response.headers}");
if (response.statusCode == 200) { if (response.statusCode == 200) {
print("✅ API call successful - Order saved to server"); print("✅ API call successful - Order saved to server");
return true; return true;
@ -69,7 +77,8 @@ class OrderRemoteDatasource {
print("✅ getOrderByRangeDate API call successful"); print("✅ getOrderByRangeDate API call successful");
return Right(OrderResponseModel.fromJson(response.body)); return Right(OrderResponseModel.fromJson(response.body));
} else { } else {
print("❌ getOrderByRangeDate API call failed - Status: ${response.statusCode}"); print(
"❌ getOrderByRangeDate API call failed - Status: ${response.statusCode}");
print("❌ Error Response: ${response.body}"); print("❌ Error Response: ${response.body}");
return const Left("Failed Load Data"); return const Left("Failed Load Data");
} }
@ -102,7 +111,8 @@ class OrderRemoteDatasource {
print("✅ getSummaryByRangeDate API call successful"); print("✅ getSummaryByRangeDate API call successful");
return Right(SummaryResponseModel.fromJson(response.body)); return Right(SummaryResponseModel.fromJson(response.body));
} else { } else {
print("❌ getSummaryByRangeDate API call failed - Status: ${response.statusCode}"); print(
"❌ getSummaryByRangeDate API call failed - Status: ${response.statusCode}");
print("❌ Error Response: ${response.body}"); print("❌ Error Response: ${response.body}");
return const Left("Failed Load Data"); return const Left("Failed Load Data");
} }
@ -112,7 +122,8 @@ class OrderRemoteDatasource {
} }
} }
Future<Either<String, PaymentMethodResponseModel>> getPaymentMethodByRangeDate( Future<Either<String, PaymentMethodResponseModel>>
getPaymentMethodByRangeDate(
String startDate, String startDate,
String endDate, String endDate,
) async { ) async {
@ -134,7 +145,8 @@ class OrderRemoteDatasource {
print("✅ getPaymentMethodByRangeDate API call successful"); print("✅ getPaymentMethodByRangeDate API call successful");
return Right(PaymentMethodResponseModel.fromJson(response.body)); return Right(PaymentMethodResponseModel.fromJson(response.body));
} else { } else {
print("❌ getPaymentMethodByRangeDate API call failed - Status: ${response.statusCode}"); print(
"❌ getPaymentMethodByRangeDate API call failed - Status: ${response.statusCode}");
print("❌ Error Response: ${response.body}"); print("❌ Error Response: ${response.body}");
return const Left("Failed Load Payment Method Data"); return const Left("Failed Load Payment Method Data");
} }
@ -162,16 +174,17 @@ class OrderRemoteDatasource {
'order_items': orderItems, 'order_items': orderItems,
}), }),
); );
print("📥 Add Order Items HTTP Status Code: ${response.statusCode}"); print("📥 Add Order Items HTTP Status Code: ${response.statusCode}");
print("📥 Add Order Items Response Body: ${response.body}"); print("📥 Add Order Items Response Body: ${response.body}");
print("📥 Add Order Items Response Headers: ${response.headers}"); print("📥 Add Order Items Response Headers: ${response.headers}");
if (response.statusCode == 200) { if (response.statusCode == 200) {
print("✅ addOrderItems API call successful"); print("✅ addOrderItems API call successful");
return const Right(true); return const Right(true);
} else { } else {
print("❌ addOrderItems API call failed - Status: ${response.statusCode}"); print(
"❌ addOrderItems API call failed - Status: ${response.statusCode}");
print("❌ Error Response: ${response.body}"); print("❌ Error Response: ${response.body}");
return Left("Failed to add order items: ${response.body}"); return Left("Failed to add order items: ${response.body}");
} }
@ -180,4 +193,432 @@ class OrderRemoteDatasource {
return Left("Failed: $e"); return Left("Failed: $e");
} }
} }
// New Api
Future<Either<String, OrderDetailResponseModel>> 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<Either<String, PaymentSuccessResponseModel>> 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<Either<String, OrderResponseModel>> 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<String, dynamic> 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<Either<String, OrderDetailResponseModel>> 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<Either<String, OrderDetailResponseModel>> 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<Either<String, bool>> addToOrder({
required String orderId,
required List<OrderItemRequest> 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<Either<String, bool>> refund({
required String orderId,
required String reason,
required List<OrderItem> 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<Either<String, bool>> voidOrder({
required String orderId,
required String reason,
String type = "ITEM", // TYPE: ALL, ITEM
required List<OrderItem> 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<Either<String, bool>> 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<Either<String, PaymentSuccessResponseModel>> 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');
}
}
} }

View File

@ -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<void> 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<void> remove() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove('outlet');
}
Future<Outlet> get() async {
final prefs = await SharedPreferences.getInstance();
final outlet = prefs.getString('outlet');
log('Outlet Local Data: $outlet');
return Outlet.fromJson(outlet!);
}
}

View File

@ -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<Either<String, OutletResponse>> 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<Either<String, OutletDetailResponse>> 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');
}
}
}

View File

@ -1,34 +1,40 @@
import 'dart:developer'; import 'dart:developer';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:dio/dio.dart';
import 'package:enaklo_pos/core/constants/variables.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/datasources/auth_local_datasource.dart';
import 'package:enaklo_pos/data/models/response/payment_methods_response_model.dart'; import 'package:enaklo_pos/data/models/response/payment_methods_response_model.dart';
import 'package:http/http.dart' as http;
class PaymentMethodsRemoteDatasource { class PaymentMethodsRemoteDatasource {
Future<Either<String, PaymentMethodsResponseModel>> getPaymentMethods() async { final Dio dio = DioClient.instance;
Future<Either<String, PaymentMethodsResponseModel>>
getPaymentMethods() async {
try { try {
final authData = await AuthLocalDataSource().getAuthData(); final authData = await AuthLocalDataSource().getAuthData();
final response = await http.get( final response = await dio.get(
Uri.parse('${Variables.baseUrl}/api/payment-methods'), '${Variables.baseUrl}/api/v1/payment-methods',
headers: { options: Options(
'Authorization': 'Bearer ${authData.token}', headers: {
'Accept': 'application/json', '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) { if (response.statusCode == 200) {
return Right(PaymentMethodsResponseModel.fromJson(response.body)); return Right(PaymentMethodsResponseModel.fromMap(response.data));
} else { } else {
return const Left('Failed to get payment methods'); 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) { } catch (e) {
log("Error getting payment methods: $e"); log("Error getting payment methods: $e");
return Left('Error: $e'); return Left('Unexpected error');
} }
} }
} }

View File

@ -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/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_item.dart';
import 'package:enaklo_pos/presentation/table/models/draft_order_model.dart'; import 'package:enaklo_pos/presentation/table/models/draft_order_model.dart';
import 'package:intl/intl.dart';
import 'package:sqflite/sqflite.dart'; import 'package:sqflite/sqflite.dart';
import '../../presentation/home/models/product_quantity.dart'; import '../../presentation/home/models/product_quantity.dart';
@ -145,31 +144,32 @@ class ProductLocalDatasource {
Future<Database> _initDB(String filePath) async { Future<Database> _initDB(String filePath) async {
final dbPath = await getDatabasesPath(); final dbPath = await getDatabasesPath();
final path = dbPath + filePath; final path = dbPath + filePath;
// Force delete existing database to ensure new schema // Force delete existing database to ensure new schema
try { try {
final dbExists = await databaseExists(path); final dbExists = await databaseExists(path);
if (dbExists) { if (dbExists) {
log("Deleting existing database to ensure new schema with order_type column"); log("Deleting existing database to ensure new schema with order_type column");
await deleteDatabase(path); // await deleteDatabase(path);
} }
} catch (e) { } catch (e) {
log("Error deleting database: $e"); log("Error deleting database: $e");
} }
return await openDatabase( return await openDatabase(
path, path,
version: 2, version: 2,
onCreate: _createDb, onCreate: _createDb,
onUpgrade: _onUpgrade, onUpgrade: _onUpgrade,
); );
} }
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async { Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
if (oldVersion < 2) { if (oldVersion < 2) {
// Add order_type column to orders table if it doesn't exist // Add order_type column to orders table if it doesn't exist
try { 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"); log("Added order_type column to orders table");
} catch (e) { } catch (e) {
log("order_type column might already exist: $e"); log("order_type column might already exist: $e");
@ -186,11 +186,11 @@ class ProductLocalDatasource {
//save order //save order
Future<int> saveOrder(OrderModel order) async { Future<int> saveOrder(OrderModel order) async {
final db = await instance.database; final db = await instance.database;
// Since we're forcing database recreation, order_type column should exist // Since we're forcing database recreation, order_type column should exist
final orderMap = order.toMap(includeOrderType: true); final orderMap = order.toMap(includeOrderType: true);
log("Final orderMap for insertion: $orderMap"); log("Final orderMap for insertion: $orderMap");
int id = await db.insert(tableOrder, orderMap, int id = await db.insert(tableOrder, orderMap,
conflictAlgorithm: ConflictAlgorithm.replace); conflictAlgorithm: ConflictAlgorithm.replace);
@ -238,6 +238,31 @@ class ProductLocalDatasource {
}); });
} }
Future<List<OrderModel>> 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<Map<String, dynamic>> maps = await db.query(
tableOrder,
where: 'substr(transaction_time, 1, 10) BETWEEN ? AND ?',
whereArgs: [startDateYYYYMMDD, endDateYYYYMMDD],
orderBy: 'transaction_time DESC',
);
log("Get All Order By Range: $startDateYYYYMMDD $endDateYYYYMMDD");
return List.generate(maps.length, (i) {
log("Save save OrderModel: ${OrderModel.fromMap(maps[i])}");
return OrderModel.fromMap(maps[i]);
});
}
//get order item by order id //get order item by order id
Future<List<ProductQuantity>> getOrderItemByOrderId(int orderId) async { Future<List<ProductQuantity>> getOrderItemByOrderId(int orderId) async {
final db = await instance.database; final db = await instance.database;
@ -283,7 +308,7 @@ class ProductLocalDatasource {
tableProduct, tableProduct,
product.toLocalMap(), product.toLocalMap(),
where: 'product_id = ?', where: 'product_id = ?',
whereArgs: [product.productId], whereArgs: [product.id],
); );
} }
@ -294,7 +319,7 @@ class ProductLocalDatasource {
for (var product in products) { for (var product in products) {
await db.insert(tableProduct, product.toLocalMap(), await db.insert(tableProduct, product.toLocalMap(),
conflictAlgorithm: ConflictAlgorithm.replace); conflictAlgorithm: ConflictAlgorithm.replace);
log('inserted success id: ${product.productId} | name: ${product.name} | price: ${product.price} | Printer Type ${product.printerType}'); log('inserted success id: ${product.id} | name: ${product.name} | price: ${product.price} ');
} }
} }
@ -333,19 +358,19 @@ class ProductLocalDatasource {
// generate table managent with count // generate table managent with count
Future<void> createTableManagement(String tableName, Offset position) async { Future<void> createTableManagement(String tableName, Offset position) async {
final db = await instance.database; // final db = await instance.database;
TableModel newTable = TableModel( // TableModel newTable = TableModel(
tableName: tableName, // tableName: tableName,
status: 'available', // status: 'available',
orderId: 0, // orderId: 0,
paymentAmount: 0, // paymentAmount: 0,
startTime: DateTime.now().toIso8601String(), // startTime: DateTime.now().toIso8601String(),
position: position, // position: position,
); // );
await db.insert( // await db.insert(
tableManagement, // tableManagement,
newTable.toMap(), // newTable.toMap(),
); // );
} }
// change position table // change position table
@ -402,12 +427,12 @@ class ProductLocalDatasource {
Future<List<TableModel>> getTableByStatus(String status) async { Future<List<TableModel>> getTableByStatus(String status) async {
final db = await instance.database; final db = await instance.database;
List<Map<String, dynamic>> maps; List<Map<String, dynamic>> maps;
if (status == 'all') { if (status == 'all') {
// Get all tables // Get all tables
maps = await db.query(tableManagement); maps = await db.query(tableManagement);
log("Getting all tables, found: ${maps.length}"); log("Getting all tables, found: ${maps.length}");
// If no tables exist, create some default tables // If no tables exist, create some default tables
if (maps.isEmpty) { if (maps.isEmpty) {
log("No tables found, creating default tables..."); log("No tables found, creating default tables...");
@ -428,19 +453,19 @@ class ProductLocalDatasource {
final tables = List.generate(maps.length, (i) { final tables = List.generate(maps.length, (i) {
return TableModel.fromMap(maps[i]); return TableModel.fromMap(maps[i]);
}); });
log("Returning ${tables.length} tables"); log("Returning ${tables.length} tables");
tables.forEach((table) { tables.forEach((table) {
log("Table: ${table.tableName} (ID: ${table.id}, Status: ${table.status})"); log("Table: ${table.tableName} (ID: ${table.id}, Status: ${table.status})");
}); });
return tables; return tables;
} }
// Create default tables if none exist // Create default tables if none exist
Future<void> _createDefaultTables() async { Future<void> _createDefaultTables() async {
final db = await instance.database; final db = await instance.database;
// Create 5 default tables // Create 5 default tables
for (int i = 1; i <= 5; i++) { for (int i = 1; i <= 5; i++) {
await db.insert(tableManagement, { await db.insert(tableManagement, {
@ -463,7 +488,7 @@ class ProductLocalDatasource {
await db.update(tableManagement, table.toMap(), await db.update(tableManagement, table.toMap(),
where: 'id = ?', whereArgs: [table.id]); where: 'id = ?', whereArgs: [table.id]);
log("Success Update Status Table: ${table.toMap()}"); log("Success Update Status Table: ${table.toMap()}");
// Verify the update // Verify the update
final updatedTable = await db.query( final updatedTable = await db.query(
tableManagement, tableManagement,
@ -474,7 +499,7 @@ class ProductLocalDatasource {
log("Verified table update: ${updatedTable.first}"); log("Verified table update: ${updatedTable.first}");
} }
} }
// Debug method to reset all tables to available status // Debug method to reset all tables to available status
Future<void> resetAllTablesToAvailable() async { Future<void> resetAllTablesToAvailable() async {
log("Resetting all tables to available status..."); log("Resetting all tables to available status...");
@ -564,7 +589,7 @@ class ProductLocalDatasource {
//update draft order //update draft order
Future<void> updateDraftOrder(DraftOrderModel draftOrder) async { Future<void> updateDraftOrder(DraftOrderModel draftOrder) async {
final db = await instance.database; final db = await instance.database;
// Update the draft order // Update the draft order
await db.update( await db.update(
'draft_orders', 'draft_orders',
@ -572,13 +597,14 @@ class ProductLocalDatasource {
where: 'id = ?', where: 'id = ?',
whereArgs: [draftOrder.id], whereArgs: [draftOrder.id],
); );
// Remove existing items and add new ones // Remove existing items and add new ones
await db.delete('draft_order_items', await db.delete('draft_order_items',
where: 'id_draft_order = ?', whereArgs: [draftOrder.id]); where: 'id_draft_order = ?', whereArgs: [draftOrder.id]);
for (var orderItem in draftOrder.orders) { for (var orderItem in draftOrder.orders) {
await db.insert('draft_order_items', orderItem.toMapForLocal(draftOrder.id!)); await db.insert(
'draft_order_items', orderItem.toMapForLocal(draftOrder.id!));
} }
} }

View File

@ -1,94 +1,126 @@
import 'dart:developer'; import 'dart:developer';
import 'package:dartz/dartz.dart'; 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/request/product_request_model.dart'; import 'package:enaklo_pos/data/models/request/product_request_model.dart';
import 'package:enaklo_pos/data/models/response/add_product_response_model.dart'; import 'package:enaklo_pos/data/models/response/add_product_response_model.dart';
import 'package:enaklo_pos/data/models/response/product_response_model.dart'; import 'package:enaklo_pos/data/models/response/product_response_model.dart';
import 'package:http/http.dart' as http;
import '../../core/constants/variables.dart'; import '../../core/constants/variables.dart';
import 'auth_local_datasource.dart'; import 'auth_local_datasource.dart';
class ProductRemoteDatasource { class ProductRemoteDatasource {
Future<Either<String, ProductResponseModel>> getProducts() async { final Dio dio = DioClient.instance;
final url = Uri.parse('${Variables.baseUrl}/api/products');
final authData = await AuthLocalDataSource().getAuthData(); Future<Either<String, ProductResponseModel>> getProducts({
final response = await http.get(url, headers: { int page = 1,
'Authorization': 'Bearer ${authData.token}', int limit = Variables.defaultLimit,
'Accept': 'application/json', String? categoryId,
}); String? search,
log("Status Code: ${response.statusCode}"); }) async {
log("Body: ${response.body}"); try {
if (response.statusCode == 200) { final authData = await AuthLocalDataSource().getAuthData();
return Right(ProductResponseModel.fromJson(response.body)); final url = '${Variables.baseUrl}/api/v1/products';
} else {
return const Left('Failed to get products'); Map<String, dynamic> queryParameters = {
'page': page,
'limit': limit,
};
if (categoryId != null) {
queryParameters['category_id'] = categoryId;
}
if (search != null && search.isNotEmpty) {
queryParameters['search'] = search;
}
final response = await dio.get(
url,
queryParameters: queryParameters,
options: Options(
headers: {
'Authorization': 'Bearer ${authData.token}',
'Accept': 'application/json',
},
),
);
if (response.statusCode == 200) {
return Right(ProductResponseModel.fromMap(response.data));
} else {
return const Left('Failed to get products');
}
} on DioException catch (e) {
log("Dio error: ${e.message}");
return Left(e.response?.data['message'] ?? 'Gagal mengambil produk');
} catch (e) {
log("Unexpected error: $e");
return const Left('Unexpected error occurred');
} }
} }
Future<Either<String, AddProductResponseModel>> addProduct( Future<Either<String, AddProductResponseModel>> addProduct(
ProductRequestModel productRequestModel) async { ProductRequestModel productRequestModel) async {
final authData = await AuthLocalDataSource().getAuthData(); try {
final Map<String, String> headers = { final authData = await AuthLocalDataSource().getAuthData();
'Authorization': 'Bearer ${authData.token}', final url = '${Variables.baseUrl}/api/v1/products';
};
var request = http.MultipartRequest(
'POST', Uri.parse('${Variables.baseUrl}/api/products'));
request.fields.addAll(productRequestModel.toMap());
request.files.add(await http.MultipartFile.fromPath(
'image', productRequestModel.image!.path));
request.headers.addAll(headers);
http.StreamedResponse response = await request.send(); final response = await dio.post(
url,
data: productRequestModel.toMap(),
options: Options(
headers: {
'Authorization': 'Bearer ${authData.token}',
'Accept': 'application/json',
},
),
);
final String body = await response.stream.bytesToString(); if (response.statusCode == 200) {
log(response.stream.toString()); return Right(AddProductResponseModel.fromMap(response.data));
log(response.statusCode.toString()); } else {
if (response.statusCode == 201) { return const Left('Failed to create products');
return right(AddProductResponseModel.fromJson(body)); }
} else { } on DioException catch (e) {
return left(body); log("Dio error: ${e.message}");
return Left(e.response?.data['message'] ?? 'Gagal menambah produk');
} catch (e) {
log("Unexpected error: $e");
return const Left('Unexpected error occurred');
} }
} }
Future<Either<String, AddProductResponseModel>> updateProduct( Future<Either<String, AddProductResponseModel>> updateProduct(
ProductRequestModel productRequestModel) async { ProductRequestModel productRequestModel) async {
final authData = await AuthLocalDataSource().getAuthData(); try {
final Map<String, String> headers = { final authData = await AuthLocalDataSource().getAuthData();
'Authorization': 'Bearer ${authData.token}', final url =
}; '${Variables.baseUrl}/api/v1/products/${productRequestModel.id}';
log("Update Product Request Data: ${productRequestModel.toMap()}");
log("Update Product ID: ${productRequestModel.id}");
log("Update Product Name: ${productRequestModel.name}");
log("Update Product Price: ${productRequestModel.price}");
log("Update Product Stock: ${productRequestModel.stock}");
log("Update Product Category ID: ${productRequestModel.categoryId}");
log("Update Product Is Best Seller: ${productRequestModel.isBestSeller}");
log("Update Product Printer Type: ${productRequestModel.printerType}");
log("Update Product Has Image: ${productRequestModel.image != null}");
var request = http.MultipartRequest(
'POST', Uri.parse('${Variables.baseUrl}/api/products/edit'));
request.fields.addAll(productRequestModel.toMap());
if (productRequestModel.image != null) {
request.files.add(await http.MultipartFile.fromPath(
'image', productRequestModel.image!.path));
}
request.headers.addAll(headers);
log("Update Product Request Fields: ${request.fields}"); final response = await dio.put(
log("Update Product Request Files: ${request.files.length}"); url,
data: productRequestModel.toMap(),
options: Options(
headers: {
'Authorization': 'Bearer ${authData.token}',
'Accept': 'application/json',
},
),
);
http.StreamedResponse response = await request.send(); if (response.statusCode == 200) {
return Right(AddProductResponseModel.fromMap(response.data));
final String body = await response.stream.bytesToString(); } else {
log("Update Product Status Code: ${response.statusCode}"); return const Left('Failed to update products');
log("Update Product Body: $body"); }
if (response.statusCode == 200) { } on DioException catch (e) {
return right(AddProductResponseModel.fromJson(body)); log("Dio error: ${e.message}");
} else { return Left(e.response?.data['message'] ?? 'Gagal update produk');
return left(body); } catch (e) {
log("Unexpected error: $e");
return const Left('Unexpected error occurred');
} }
} }
} }

View File

@ -0,0 +1,170 @@
import 'dart:developer';
import 'dart:ui';
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/table_model.dart';
import '../../core/constants/variables.dart';
import 'auth_local_datasource.dart';
class TableRemoteDataSource {
final Dio dio = DioClient.instance;
Future<Either<String, bool>> createTable({
required String tableName,
required int capacity,
required String location,
}) async {
try {
final authData = await AuthLocalDataSource().getAuthData();
final url = '${Variables.baseUrl}/api/v1/tables';
final response = await dio.post(
url,
data: {
"outlet_id": authData.user?.outletId,
"table_name": tableName,
"capacity": capacity,
"location": location,
"status": "available",
"is_active": true,
"position_x": 200,
"position_y": 200,
},
options: Options(
headers: {
'Authorization': 'Bearer ${authData.token}',
'Accept': 'application/json',
},
),
);
if (response.statusCode == 200 || response.statusCode == 201) {
return Right(true);
} else {
return const Left('Failed to create table');
}
} on DioException catch (e) {
log("Dio error: ${e.message}");
return Left(e.response?.data['message'] ?? 'Gagal membuat table');
} catch (e) {
log("Unexpected error: $e");
return const Left('Unexpected error occurred');
}
}
Future<Either<String, TableResponseModel>> getTable({
int page = 1,
int limit = 50,
String? status,
}) async {
try {
final authData = await AuthLocalDataSource().getAuthData();
final url = '${Variables.baseUrl}/api/v1/tables';
Map<String, dynamic> queryParameters = {
'page': page,
'limit': limit,
};
if (status != null) {
queryParameters['status'] = status;
}
final response = await dio.get(
url,
queryParameters: queryParameters,
options: Options(
headers: {
'Authorization': 'Bearer ${authData.token}',
'Accept': 'application/json',
},
),
);
if (response.statusCode == 200) {
return Right(TableResponseModel.fromMap(response.data));
} else {
return const Left('Failed to get tables');
}
} on DioException catch (e) {
log("Dio error: ${e.message}");
return Left(e.response?.data['message'] ?? 'Gagal mengambil data meja');
} catch (e) {
log("Unexpected error: $e");
return const Left('Unexpected error occurred');
}
}
Future<Either<String, bool>> updatePosition({
required String tableId,
required Offset position,
}) async {
try {
final authData = await AuthLocalDataSource().getAuthData();
final url = '${Variables.baseUrl}/api/v1/tables/$tableId';
final response = await dio.put(
url,
data: {
"position_x": position.dx.round(),
"position_y": position.dy.round(),
},
options: Options(
headers: {
'Authorization': 'Bearer ${authData.token}',
'Accept': 'application/json',
},
),
);
if (response.statusCode == 200 || response.statusCode == 201) {
return Right(true);
} else {
return const Left('Failed to create table');
}
} on DioException catch (e) {
log("Dio error: ${e.message}");
return Left(e.response?.data['message'] ?? 'Gagal membuat table');
} catch (e) {
log("Unexpected error: $e");
return const Left('Unexpected error occurred');
}
}
Future<Either<String, bool>> transferTable({
required String fromTableId,
required String toTableId,
}) async {
try {
final authData = await AuthLocalDataSource().getAuthData();
final url = '${Variables.baseUrl}/api/v1/tables/transfer';
final response = await dio.put(
url,
data: {
"from_table": fromTableId,
"to_table": toTableId,
},
options: Options(
headers: {
'Authorization': 'Bearer ${authData.token}',
'Accept': 'application/json',
},
),
);
if (response.statusCode == 200 || response.statusCode == 201) {
return Right(true);
} else {
return const Left('Failed to create table');
}
} on DioException catch (e) {
log("Dio error: ${e.message}");
return Left(e.response?.data['message'] ?? 'Gagal membuat table');
} catch (e) {
log("Unexpected error: $e");
return const Left('Unexpected error occurred');
}
}
}

View File

@ -0,0 +1,47 @@
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';
class UserRemoteDatasource {
final Dio dio = DioClient.instance;
Future<Either<String, bool>> updateOutlet(String outletId) async {
AuthResponseModel authData = await AuthLocalDataSource().getAuthData();
final url = '${Variables.baseUrl}/api/v1/users/${authData.user?.id}';
try {
final response = await dio.put(
url,
data: {
'outlet_id': outletId,
},
options: Options(
headers: {
'Authorization': 'Bearer ${authData.token}',
'Accept': 'application/json',
'Content-Type': 'application/json',
},
),
);
if (response.statusCode == 200) {
authData.user?.outletId = response.data['outlet_id'];
await AuthLocalDataSource().saveAuthData(authData);
return Right(true);
} 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');
}
}
}

View File

@ -0,0 +1,137 @@
import 'dart:convert';
class PaymentRequestModel {
final String? orderId;
final String? paymentMethodId;
final int? amount;
final String? transactionId;
final int? splitNumber;
final int? splitTotal;
final String? splitDescription;
final List<PaymentOrderItemModel>? paymentOrderItems;
PaymentRequestModel({
this.orderId,
this.paymentMethodId,
this.amount,
this.transactionId,
this.splitNumber,
this.splitTotal,
this.splitDescription,
this.paymentOrderItems,
});
factory PaymentRequestModel.fromJson(String str) =>
PaymentRequestModel.fromMap(json.decode(str));
String toJson() => json.encode(toMap());
factory PaymentRequestModel.fromMap(Map<String, dynamic> json) =>
PaymentRequestModel(
orderId: json["order_id"],
paymentMethodId: json["payment_method_id"],
amount: json["amount"]?.toDouble(),
transactionId: json["transaction_id"],
splitNumber: json["split_number"],
splitTotal: json["split_total"],
splitDescription: json["split_description"],
paymentOrderItems: json["payment_order_items"] == null
? []
: List<PaymentOrderItemModel>.from(json["payment_order_items"]
.map((x) => PaymentOrderItemModel.fromMap(x))),
);
Map<String, dynamic> toMap() => {
"order_id": orderId,
"payment_method_id": paymentMethodId,
"amount": amount,
"transaction_id": transactionId,
"split_number": splitNumber,
"split_total": splitTotal,
"split_description": splitDescription,
"payment_order_items": paymentOrderItems == null
? []
: List<dynamic>.from(paymentOrderItems!.map((x) => x.toMap())),
};
}
class PaymentOrderItemModel {
final String? orderItemId;
final int? amount;
PaymentOrderItemModel({
this.orderItemId,
this.amount,
});
factory PaymentOrderItemModel.fromJson(String str) =>
PaymentOrderItemModel.fromMap(json.decode(str));
factory PaymentOrderItemModel.fromMap(Map<String, dynamic> json) =>
PaymentOrderItemModel(
orderItemId: json["order_item_id"],
amount: json["amount"]?.toDouble(),
);
Map<String, dynamic> toMap() => {
"order_item_id": orderItemId,
"amount": amount,
};
}
class PaymentSplitBillRequest {
final String orderId;
final String paymentMethodId;
final String customerId;
final String type; // e.g., "AMOUNT" or "ITEM"
final int amount;
final List<SplitItem> items;
PaymentSplitBillRequest({
required this.orderId,
required this.paymentMethodId,
required this.customerId,
required this.type,
required this.amount,
required this.items,
});
Map<String, dynamic> toMap() {
Map<String, dynamic> data = {
'order_id': orderId,
'payment_method_id': paymentMethodId,
'type': type,
};
if (customerId.isNotEmpty) {
data['customer_id'] = customerId;
}
if (type == "AMOUNT") {
data['amount'] = amount;
}
if (type == "ITEM") {
data['items'] = items.map((e) => e.toJson()).toList();
}
return data;
}
}
class SplitItem {
final String orderItemId;
final int quantity;
SplitItem({
required this.orderItemId,
required this.quantity,
});
Map<String, dynamic> toJson() {
return {
'order_item_id': orderItemId,
'quantity': quantity,
};
}
}

View File

@ -1,42 +1,51 @@
import 'dart:developer';
import 'package:image_picker/image_picker.dart';
class ProductRequestModel { class ProductRequestModel {
final int? id; final String? id;
final String name; final String name;
final String? description;
final String categoryId;
final String? sku;
final String? barcode;
final int price; final int price;
final int stock; final int cost;
final int categoryId; final bool isActive;
final int isBestSeller; final bool hasVariants;
final XFile? image; final String imageUrl;
final String? printerType; final String? printerType;
ProductRequestModel({ ProductRequestModel({
this.id, this.id,
required this.name, required this.name,
required this.price, this.description,
required this.stock,
required this.categoryId, required this.categoryId,
required this.isBestSeller, this.sku,
this.image, this.barcode,
required this.price,
required this.cost,
this.isActive = true,
this.hasVariants = false,
required this.imageUrl,
this.printerType, this.printerType,
}); });
Map<String, String> toMap() { Map<String, dynamic> toMap() {
log("toMap: $isBestSeller"); final map = <String, dynamic>{
final map = {
'name': name, 'name': name,
'price': price.toString(), 'description': description ?? '',
'stock': stock.toString(), 'category_id': categoryId,
'category_id': categoryId.toString(), 'sku': sku ?? '',
'is_best_seller': isBestSeller.toString(), 'barcode': barcode ?? '',
'price': price,
'cost': cost,
'is_active': isActive,
'has_variants': hasVariants,
'image_url': imageUrl,
'printer_type': printerType ?? '', 'printer_type': printerType ?? '',
}; };
if (id != null) { if (id != null) {
map['id'] = id.toString(); map['id'] = id;
} }
return map; return map;
} }
} }

View File

@ -4,12 +4,10 @@ import 'package:enaklo_pos/data/models/response/product_response_model.dart';
class AddProductResponseModel { class AddProductResponseModel {
final bool success; final bool success;
final String message;
final Product data; final Product data;
AddProductResponseModel({ AddProductResponseModel({
required this.success, required this.success,
required this.message,
required this.data, required this.data,
}); });
@ -21,13 +19,11 @@ class AddProductResponseModel {
factory AddProductResponseModel.fromMap(Map<String, dynamic> json) => factory AddProductResponseModel.fromMap(Map<String, dynamic> json) =>
AddProductResponseModel( AddProductResponseModel(
success: json["success"], success: json["success"],
message: json["message"],
data: Product.fromMap(json["data"]), data: Product.fromMap(json["data"]),
); );
Map<String, dynamic> toMap() => { Map<String, dynamic> toMap() => {
"success": success, "success": success,
"message": message,
"data": data.toMap(), "data": data.toMap(),
}; };
} }

View File

@ -1,85 +1,83 @@
import 'dart:convert'; import 'dart:convert';
class AuthResponseModel { class AuthResponseModel {
final String? status; final String? token;
final String? token; final User? user;
final User? user;
AuthResponseModel({ AuthResponseModel({
this.status, this.token,
this.token, this.user,
this.user, });
});
factory AuthResponseModel.fromJson(String str) => AuthResponseModel.fromMap(json.decode(str)); factory AuthResponseModel.fromJson(String str) =>
AuthResponseModel.fromMap(json.decode(str));
String toJson() => json.encode(toMap()); String toJson() => json.encode(toMap());
factory AuthResponseModel.fromMap(Map<String, dynamic> json) => AuthResponseModel( factory AuthResponseModel.fromMap(Map<String, dynamic> json) =>
status: json["status"], AuthResponseModel(
token: json["token"], token: json["token"],
user: json["user"] == null ? null : User.fromMap(json["user"]), user: json["user"] == null ? null : User.fromMap(json["user"]),
); );
Map<String, dynamic> toMap() => { Map<String, dynamic> toMap() => {
"status": status,
"token": token, "token": token,
"user": user?.toMap(), "user": user?.toMap(),
}; };
} }
class User { class User {
final int? id; final String? id;
final String? name; final String? organizationId;
final String? email; String? outletId;
final DateTime? emailVerifiedAt; final String? name;
final dynamic twoFactorSecret; final String? email;
final dynamic twoFactorRecoveryCodes; final String? role;
final dynamic twoFactorConfirmedAt; final bool? isActive;
final DateTime? createdAt; final DateTime? createdAt;
final DateTime? updatedAt; final DateTime? updatedAt;
final String? role;
User({ User({
this.id, this.id,
this.name, this.organizationId,
this.email, this.outletId,
this.emailVerifiedAt, this.name,
this.twoFactorSecret, this.role,
this.twoFactorRecoveryCodes, this.isActive,
this.twoFactorConfirmedAt, this.email,
this.createdAt, this.createdAt,
this.updatedAt, this.updatedAt,
this.role, });
});
factory User.fromJson(String str) => User.fromMap(json.decode(str)); factory User.fromJson(String str) => User.fromMap(json.decode(str));
String toJson() => json.encode(toMap()); String toJson() => json.encode(toMap());
factory User.fromMap(Map<String, dynamic> json) => User( factory User.fromMap(Map<String, dynamic> json) => User(
id: json["id"], id: json["id"],
organizationId: json["organization_id"],
outletId: json["outlet_id"],
name: json["name"], name: json["name"],
email: json["email"], email: json["email"],
emailVerifiedAt: json["email_verified_at"] == null ? null : DateTime.parse(json["email_verified_at"]),
twoFactorSecret: json["two_factor_secret"],
twoFactorRecoveryCodes: json["two_factor_recovery_codes"],
twoFactorConfirmedAt: json["two_factor_confirmed_at"],
createdAt: json["created_at"] == null ? null : DateTime.parse(json["created_at"]),
updatedAt: json["updated_at"] == null ? null : DateTime.parse(json["updated_at"]),
role: json["role"], role: json["role"],
); isActive: json["is_active"],
createdAt: json["created_at"] == null
? null
: DateTime.parse(json["created_at"]),
updatedAt: json["updated_at"] == null
? null
: DateTime.parse(json["updated_at"]),
);
Map<String, dynamic> toMap() => { Map<String, dynamic> toMap() => {
"id": id, "id": id,
"organization_id": organizationId,
"outlet_id": outletId,
"name": name, "name": name,
"email": email, "email": email,
"email_verified_at": emailVerifiedAt?.toIso8601String(), "role": role,
"two_factor_secret": twoFactorSecret, "is_active": isActive,
"two_factor_recovery_codes": twoFactorRecoveryCodes,
"two_factor_confirmed_at": twoFactorConfirmedAt,
"created_at": createdAt?.toIso8601String(), "created_at": createdAt?.toIso8601String(),
"updated_at": updatedAt?.toIso8601String(), "updated_at": updatedAt?.toIso8601String(),
"role": role, };
};
} }

View File

@ -1,91 +1,120 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'dart:convert'; import 'dart:convert';
class CategroyResponseModel { class CategoryResponseModel {
final String status; final bool success;
final List<CategoryModel> data; final CategoryData data;
final dynamic errors;
CategroyResponseModel({ CategoryResponseModel({
required this.status, required this.success,
required this.data, required this.data,
this.errors,
}); });
Map<String, dynamic> toMap() { factory CategoryResponseModel.fromMap(Map<String, dynamic> map) {
return <String, dynamic>{ return CategoryResponseModel(
'status': status, success: map['success'] as bool,
'data': data.map((x) => x.toMap()).toList(), data: CategoryData.fromMap(map['data'] as Map<String, dynamic>),
}; errors: map['errors'],
}
factory CategroyResponseModel.fromMap(Map<String, dynamic> map) {
return CategroyResponseModel(
status: map['status'] as String,
data: List<CategoryModel>.from(
(map['data']).map<CategoryModel>(
(x) => CategoryModel.fromMap(x as Map<String, dynamic>),
),
),
); );
} }
factory CategroyResponseModel.fromJson(String str) => factory CategoryResponseModel.fromJson(String str) =>
CategroyResponseModel.fromMap(json.decode(str)); CategoryResponseModel.fromMap(json.decode(str));
String toJson() => json.encode(toMap());
}
class CategoryModel {
int? id;
String? name;
int? categoryId;
int? isSync;
String? image;
// DateTime createdAt;
// DateTime updatedAt;
CategoryModel({this.id, this.name, this.categoryId, this.isSync, this.image});
Map<String, dynamic> toMap() { Map<String, dynamic> toMap() {
return <String, dynamic>{ return {
// 'id': id, 'success': success,
'name': name, 'data': data.toMap(),
'is_sync': isSync ?? 1, 'errors': errors,
'category_id': id,
'image': image
}; };
} }
factory CategoryModel.fromMap(Map<String, dynamic> map) {
return CategoryModel(
id: map['id'] as int?,
name: map['name'] as String?,
isSync: map['is_sync'] as int?,
categoryId: map['id'],
image: map['image']);
}
factory CategoryModel.fromJson(String str) =>
CategoryModel.fromMap(json.decode(str));
String toJson() => json.encode(toMap()); String toJson() => json.encode(toMap());
}
@override class CategoryData {
bool operator ==(covariant CategoryModel other) { final List<CategoryModel> categories;
if (identical(this, other)) return true; final int totalCount;
final int page;
final int limit;
final int totalPages;
return other.id == id && CategoryData({
other.name == name && required this.categories,
other.categoryId == categoryId && required this.totalCount,
other.isSync == isSync && required this.page,
other.image == image; required this.limit,
required this.totalPages,
});
factory CategoryData.fromMap(Map<String, dynamic> map) {
return CategoryData(
categories: List<CategoryModel>.from(
(map['categories'] as List).map((x) => CategoryModel.fromMap(x)),
),
totalCount: map['total_count'] as int,
page: map['page'] as int,
limit: map['limit'] as int,
totalPages: map['total_pages'] as int,
);
} }
@override Map<String, dynamic> toMap() {
int get hashCode { return {
return id.hashCode ^ 'categories': categories.map((x) => x.toMap()).toList(),
name.hashCode ^ 'total_count': totalCount,
categoryId.hashCode ^ 'page': page,
isSync.hashCode ^ 'limit': limit,
image.hashCode; 'total_pages': totalPages,
};
}
}
class CategoryModel {
String id;
final String organizationId;
final String name;
final String? description;
final String businessType;
final Map<String, dynamic> metadata;
final DateTime createdAt;
final DateTime updatedAt;
CategoryModel({
required this.id,
required this.organizationId,
required this.name,
this.description,
required this.businessType,
required this.metadata,
required this.createdAt,
required this.updatedAt,
});
factory CategoryModel.fromMap(Map<String, dynamic> map) {
return CategoryModel(
id: map['id'] as String,
organizationId: map['organization_id'] as String,
name: map['name'] as String,
description: map['description'] as String?,
businessType: map['business_type'] as String,
metadata: Map<String, dynamic>.from(map['metadata'] ?? {}),
createdAt: DateTime.parse(map['created_at'] as String),
updatedAt: DateTime.parse(map['updated_at'] as String),
);
}
Map<String, dynamic> toMap() {
return {
'id': id,
'organization_id': organizationId,
'name': name,
'description': description,
'business_type': businessType,
'metadata': metadata,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
};
} }
} }

View File

@ -0,0 +1,127 @@
import 'dart:convert';
class CustomerResponseModel {
final bool? success;
final CustomerData? data;
final dynamic errors;
CustomerResponseModel({
this.success,
this.data,
this.errors,
});
factory CustomerResponseModel.fromJson(String str) =>
CustomerResponseModel.fromMap(json.decode(str));
String toJson() => json.encode(toMap());
factory CustomerResponseModel.fromMap(Map<String, dynamic> json) =>
CustomerResponseModel(
success: json["success"],
data: json["data"] == null ? null : CustomerData.fromMap(json["data"]),
errors: json["errors"],
);
Map<String, dynamic> toMap() => {
"success": success,
"data": data?.toMap(),
"errors": errors,
};
}
class CustomerData {
final List<Customer>? customers;
final int? totalCount;
final int? page;
final int? limit;
final int? totalPages;
CustomerData({
this.customers,
this.totalCount,
this.page,
this.limit,
this.totalPages,
});
factory CustomerData.fromMap(Map<String, dynamic> json) => CustomerData(
customers: json["data"] == null
? []
: List<Customer>.from(json["data"].map((x) => Customer.fromMap(x))),
totalCount: json["total_count"],
page: json["page"],
limit: json["limit"],
totalPages: json["total_pages"],
);
Map<String, dynamic> toMap() => {
"data": customers == null
? []
: List<dynamic>.from(customers!.map((x) => x.toMap())),
"total_count": totalCount,
"page": page,
"limit": limit,
"total_pages": totalPages,
};
}
class Customer {
final String? id;
final String? organizationId;
final String? name;
final String? email;
final String? phone;
final String? address;
final bool? isDefault;
final bool? isActive;
final Map<String, dynamic>? metadata;
final DateTime? createdAt;
final DateTime? updatedAt;
Customer({
this.id,
this.organizationId,
this.name,
this.email,
this.phone,
this.address,
this.isDefault,
this.isActive,
this.metadata,
this.createdAt,
this.updatedAt,
});
factory Customer.fromMap(Map<String, dynamic> json) => Customer(
id: json["id"],
organizationId: json["organization_id"],
name: json["name"],
email: json["email"],
phone: json["phone"],
address: json["address"],
isDefault: json["is_default"],
isActive: json["is_active"],
metadata: json["metadata"] ?? {},
createdAt: json["created_at"] == null
? null
: DateTime.parse(json["created_at"]),
updatedAt: json["updated_at"] == null
? null
: DateTime.parse(json["updated_at"]),
);
Map<String, dynamic> toMap() => {
"id": id,
"organization_id": organizationId,
"name": name,
"email": email,
"phone": phone,
"address": address,
"is_default": isDefault,
"is_active": isActive,
"metadata": metadata,
"created_at": createdAt?.toIso8601String(),
"updated_at": updatedAt?.toIso8601String(),
};
}

View File

@ -0,0 +1,248 @@
class DashboardAnalyticResponseModel {
final bool success;
final DashboardAnalyticData data;
final dynamic errors;
DashboardAnalyticResponseModel({
required this.success,
required this.data,
this.errors,
});
/// Khusus untuk JSON string
factory DashboardAnalyticResponseModel.fromJson(Map<String, dynamic> json) =>
DashboardAnalyticResponseModel.fromMap(json);
/// Untuk menerima Map biasa (bukan dari JSON string)
factory DashboardAnalyticResponseModel.fromMap(Map<String, dynamic> map) {
return DashboardAnalyticResponseModel(
success: map['success'] ?? false,
data: DashboardAnalyticData.fromMap(map['data'] ?? {}),
errors: map['errors'],
);
}
Map<String, dynamic> toJson() => toMap();
Map<String, dynamic> toMap() => {
'success': success,
'data': data.toMap(),
'errors': errors,
};
}
class DashboardAnalyticData {
final String organizationId;
final String outletId;
final String dateFrom;
final String dateTo;
final DashboardOverview overview;
final List<TopProduct> topProducts;
final List<PaymentMethodAnalytic> paymentMethods;
final List<RecentSale> recentSales;
DashboardAnalyticData({
required this.organizationId,
required this.outletId,
required this.dateFrom,
required this.dateTo,
required this.overview,
required this.topProducts,
required this.paymentMethods,
required this.recentSales,
});
factory DashboardAnalyticData.fromMap(Map<String, dynamic> map) =>
DashboardAnalyticData(
organizationId: map['organization_id'],
outletId: map['outlet_id'],
dateFrom: map['date_from'],
dateTo: map['date_to'],
overview: DashboardOverview.fromMap(map['overview']),
topProducts: map['top_products'] == null
? []
: List<TopProduct>.from(
map['top_products']?.map((x) => TopProduct.fromMap(x))),
paymentMethods: map['payment_methods'] == null
? []
: List<PaymentMethodAnalytic>.from(map['payment_methods']
?.map((x) => PaymentMethodAnalytic.fromMap(x))),
recentSales: map['recent_sales'] == null
? []
: List<RecentSale>.from(
map['recent_sales']?.map((x) => RecentSale.fromMap(x))),
);
Map<String, dynamic> toMap() => {
'organization_id': organizationId,
'outlet_id': outletId,
'date_from': dateFrom,
'date_to': dateTo,
'overview': overview.toMap(),
'top_products': topProducts.map((x) => x.toMap()).toList(),
'payment_methods': paymentMethods.map((x) => x.toMap()).toList(),
'recent_sales': recentSales.map((x) => x.toMap()).toList(),
};
}
class DashboardOverview {
final int totalSales;
final int totalOrders;
final double averageOrderValue;
final int totalCustomers;
final int voidedOrders;
final int refundedOrders;
DashboardOverview({
required this.totalSales,
required this.totalOrders,
required this.averageOrderValue,
required this.totalCustomers,
required this.voidedOrders,
required this.refundedOrders,
});
factory DashboardOverview.fromMap(Map<String, dynamic> map) =>
DashboardOverview(
totalSales: map['total_sales'],
totalOrders: map['total_orders'],
averageOrderValue: map['average_order_value']?.toDouble() ?? 0.0,
totalCustomers: map['total_customers'],
voidedOrders: map['voided_orders'],
refundedOrders: map['refunded_orders'],
);
Map<String, dynamic> toMap() => {
'total_sales': totalSales,
'total_orders': totalOrders,
'average_order_value': averageOrderValue,
'total_customers': totalCustomers,
'voided_orders': voidedOrders,
'refunded_orders': refundedOrders,
};
}
class TopProduct {
final String productId;
final String productName;
final String categoryId;
final String categoryName;
final int quantitySold;
final int revenue;
final double averagePrice;
final int orderCount;
TopProduct({
required this.productId,
required this.productName,
required this.categoryId,
required this.categoryName,
required this.quantitySold,
required this.revenue,
required this.averagePrice,
required this.orderCount,
});
factory TopProduct.fromMap(Map<String, dynamic> map) => TopProduct(
productId: map['product_id'],
productName: map['product_name'],
categoryId: map['category_id'],
categoryName: map['category_name'],
quantitySold: map['quantity_sold'],
revenue: map['revenue'],
averagePrice: map['average_price']?.toDouble() ?? 0.0,
orderCount: map['order_count'],
);
Map<String, dynamic> toMap() => {
'product_id': productId,
'product_name': productName,
'category_id': categoryId,
'category_name': categoryName,
'quantity_sold': quantitySold,
'revenue': revenue,
'average_price': averagePrice,
'order_count': orderCount,
};
}
class PaymentMethodAnalytic {
final String paymentMethodId;
final String paymentMethodName;
final String paymentMethodType;
final int totalAmount;
final int orderCount;
final int paymentCount;
final double percentage;
PaymentMethodAnalytic({
required this.paymentMethodId,
required this.paymentMethodName,
required this.paymentMethodType,
required this.totalAmount,
required this.orderCount,
required this.paymentCount,
required this.percentage,
});
factory PaymentMethodAnalytic.fromMap(Map<String, dynamic> map) =>
PaymentMethodAnalytic(
paymentMethodId: map['payment_method_id'],
paymentMethodName: map['payment_method_name'],
paymentMethodType: map['payment_method_type'],
totalAmount: map['total_amount'],
orderCount: map['order_count'],
paymentCount: map['payment_count'],
percentage: map['percentage']?.toDouble() ?? 0.0,
);
Map<String, dynamic> toMap() => {
'payment_method_id': paymentMethodId,
'payment_method_name': paymentMethodName,
'payment_method_type': paymentMethodType,
'total_amount': totalAmount,
'order_count': orderCount,
'payment_count': paymentCount,
'percentage': percentage,
};
}
class RecentSale {
final String date;
final int sales;
final int orders;
final int items;
final int tax;
final int discount;
final int netSales;
RecentSale({
required this.date,
required this.sales,
required this.orders,
required this.items,
required this.tax,
required this.discount,
required this.netSales,
});
factory RecentSale.fromMap(Map<String, dynamic> map) => RecentSale(
date: map['date'],
sales: map['sales'],
orders: map['orders'],
items: map['items'],
tax: map['tax'],
discount: map['discount'],
netSales: map['net_sales'],
);
Map<String, dynamic> toMap() => {
'date': date,
'sales': sales,
'orders': orders,
'items': items,
'tax': tax,
'discount': discount,
'net_sales': netSales,
};
}

View File

@ -0,0 +1,11 @@
class DeliveryModel {
String id;
String name;
String imageUrl;
DeliveryModel({
required this.id,
required this.name,
required this.imageUrl,
});
}

View File

@ -0,0 +1,154 @@
class FileResponseModel {
final FileModel data;
final String message;
final bool success;
FileResponseModel({
required this.data,
required this.message,
required this.success,
});
factory FileResponseModel.fromJson(Map<String, dynamic> json) {
return FileResponseModel(
data: FileModel.fromJson(json['data']),
message: json['message'] as String,
success: json['success'] as bool,
);
}
Map<String, dynamic> toJson() => {
'data': data.toJson(),
'message': message,
'success': success,
};
factory FileResponseModel.fromMap(Map<String, dynamic> map) =>
FileResponseModel.fromJson(map);
Map<String, dynamic> toMap() => toJson();
FileResponseModel copyWith({
FileModel? data,
String? message,
bool? success,
}) {
return FileResponseModel(
data: data ?? this.data,
message: message ?? this.message,
success: success ?? this.success,
);
}
@override
String toString() =>
'FileResponseModel(data: $data, message: $message, success: $success)';
}
class FileModel {
final String id;
final String organizationId;
final String userId;
final String fileName;
final String originalName;
final String fileUrl;
final int fileSize;
final String mimeType;
final String fileType;
final String uploadPath;
final bool isPublic;
final DateTime createdAt;
final DateTime updatedAt;
FileModel({
required this.id,
required this.organizationId,
required this.userId,
required this.fileName,
required this.originalName,
required this.fileUrl,
required this.fileSize,
required this.mimeType,
required this.fileType,
required this.uploadPath,
required this.isPublic,
required this.createdAt,
required this.updatedAt,
});
factory FileModel.fromJson(Map<String, dynamic> json) {
return FileModel(
id: json['id'] as String,
organizationId: json['organization_id'] as String,
userId: json['user_id'] as String,
fileName: json['file_name'] as String,
originalName: json['original_name'] as String,
fileUrl: json['file_url'] as String,
fileSize: json['file_size'] as int,
mimeType: json['mime_type'] as String,
fileType: json['file_type'] as String,
uploadPath: json['upload_path'] as String,
isPublic: json['is_public'] as bool,
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
);
}
Map<String, dynamic> toJson() => {
'id': id,
'organization_id': organizationId,
'user_id': userId,
'file_name': fileName,
'original_name': originalName,
'file_url': fileUrl,
'file_size': fileSize,
'mime_type': mimeType,
'file_type': fileType,
'upload_path': uploadPath,
'is_public': isPublic,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
};
factory FileModel.fromMap(Map<String, dynamic> map) =>
FileModel.fromJson(map);
Map<String, dynamic> toMap() => toJson();
FileModel copyWith({
String? id,
String? organizationId,
String? userId,
String? fileName,
String? originalName,
String? fileUrl,
int? fileSize,
String? mimeType,
String? fileType,
String? uploadPath,
bool? isPublic,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return FileModel(
id: id ?? this.id,
organizationId: organizationId ?? this.organizationId,
userId: userId ?? this.userId,
fileName: fileName ?? this.fileName,
originalName: originalName ?? this.originalName,
fileUrl: fileUrl ?? this.fileUrl,
fileSize: fileSize ?? this.fileSize,
mimeType: mimeType ?? this.mimeType,
fileType: fileType ?? this.fileType,
uploadPath: uploadPath ?? this.uploadPath,
isPublic: isPublic ?? this.isPublic,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
@override
String toString() {
return 'FileModel(id: $id, organizationId: $organizationId, userId: $userId, fileName: $fileName, originalName: $originalName, fileUrl: $fileUrl, fileSize: $fileSize, mimeType: $mimeType, fileType: $fileType, uploadPath: $uploadPath, isPublic: $isPublic, createdAt: $createdAt, updatedAt: $updatedAt)';
}
}

View File

@ -1,113 +0,0 @@
import 'dart:convert';
class OrderResponseModel {
String? status;
List<ItemOrder>? data;
OrderResponseModel({
this.status,
this.data,
});
factory OrderResponseModel.fromJson(String str) =>
OrderResponseModel.fromMap(json.decode(str));
String toJson() => json.encode(toMap());
factory OrderResponseModel.fromMap(Map<String, dynamic> json) =>
OrderResponseModel(
status: json["status"],
data: json["data"] == null
? []
: List<ItemOrder>.from(
json["data"]!.map((x) => ItemOrder.fromMap(x))),
);
Map<String, dynamic> toMap() => {
"status": status,
"data":
data == null ? [] : List<dynamic>.from(data!.map((x) => x.toMap())),
};
}
class ItemOrder {
int? id;
int? paymentAmount;
int? subTotal;
int? tax;
int? discount;
String? discountAmount;
int? serviceCharge;
int? total;
String? paymentMethod;
int? totalItem;
int? idKasir;
String? namaKasir;
DateTime? transactionTime;
DateTime? createdAt;
DateTime? updatedAt;
ItemOrder({
this.id,
this.paymentAmount,
this.subTotal,
this.tax,
this.discount,
this.discountAmount,
this.serviceCharge,
this.total,
this.paymentMethod,
this.totalItem,
this.idKasir,
this.namaKasir,
this.transactionTime,
this.createdAt,
this.updatedAt,
});
factory ItemOrder.fromJson(String str) => ItemOrder.fromMap(json.decode(str));
String toJson() => json.encode(toMap());
factory ItemOrder.fromMap(Map<String, dynamic> json) => ItemOrder(
id: json["id"],
paymentAmount: json["payment_amount"],
subTotal: json["sub_total"],
tax: json["tax"],
discount: json["discount"],
discountAmount: json["discount_amount"],
serviceCharge: json["service_charge"],
total: json["total"],
paymentMethod: json["payment_method"]!,
totalItem: json["total_item"],
idKasir: json["id_kasir"],
namaKasir: json["nama_kasir"],
transactionTime: json["transaction_time"] == null
? null
: DateTime.parse(json["transaction_time"]),
createdAt: json["created_at"] == null
? null
: DateTime.parse(json["created_at"]),
updatedAt: json["updated_at"] == null
? null
: DateTime.parse(json["updated_at"]),
);
Map<String, dynamic> toMap() => {
"id": id,
"payment_amount": paymentAmount,
"sub_total": subTotal,
"tax": tax,
"discount": discount,
"discount_amount": discountAmount,
"service_charge": serviceCharge,
"total": total,
"payment_method": paymentMethod,
"total_item": totalItem,
"id_kasir": idKasir,
"nama_kasir": namaKasir,
"transaction_time": transactionTime?.toIso8601String(),
"created_at": createdAt?.toIso8601String(),
"updated_at": updatedAt?.toIso8601String(),
};
}

View File

@ -0,0 +1,382 @@
import 'dart:convert';
class OrderDetailResponseModel {
final bool? success;
final Order? data;
final dynamic errors;
OrderDetailResponseModel({
this.success,
this.data,
this.errors,
});
factory OrderDetailResponseModel.fromJson(String str) =>
OrderDetailResponseModel.fromMap(json.decode(str));
String toJson() => json.encode(toMap());
factory OrderDetailResponseModel.fromMap(Map<String, dynamic> json) =>
OrderDetailResponseModel(
success: json["success"],
data: json["data"] == null ? null : Order.fromMap(json["data"]),
errors: json["errors"],
);
Map<String, dynamic> toMap() => {
"success": success,
"data": data?.toMap(),
"errors": errors,
};
}
class OrderResponseModel {
final bool? success;
final OrderData? data;
OrderResponseModel({
this.success,
this.data,
});
factory OrderResponseModel.fromJson(String str) =>
OrderResponseModel.fromMap(json.decode(str));
String toJson() => json.encode(toMap());
factory OrderResponseModel.fromMap(Map<String, dynamic> map) {
return OrderResponseModel(
success: map['success'] ?? false,
data: OrderData.fromMap(map['data']),
);
}
Map<String, dynamic> toMap() {
return {
'success': success,
'data': data?.toMap(),
};
}
}
class OrderData {
final List<Order>? orders;
final List<Payment>? payments;
final int? totalCount;
final int? page;
final int? limit;
final int? totalPages;
OrderData({
required this.orders,
required this.payments,
required this.totalCount,
required this.page,
required this.limit,
required this.totalPages,
});
factory OrderData.fromMap(Map<String, dynamic> map) {
return OrderData(
orders: map["orders"] == null
? []
: List<Order>.from(map['orders']?.map((x) => Order.fromMap(x))),
payments: map["payments"] == null
? []
: List<Payment>.from(map['payments']?.map((x) => Payment.fromMap(x))),
totalCount: map['total_count'],
page: map['page'],
limit: map['limit'],
totalPages: map['total_pages'],
);
}
Map<String, dynamic> toMap() {
return {
'orders': orders?.map((x) => x.toMap()).toList(),
'payments': payments?.map((x) => x.toMap()).toList(),
'total_count': totalCount,
'page': page,
'limit': limit,
'total_pages': totalPages,
};
}
}
class Order {
final String? id;
final String? orderNumber;
final String? outletId;
final String? userId;
final String? tableNumber;
final String? orderType;
final String? status;
final int? subtotal;
final int? taxAmount;
final int? discountAmount;
final int? totalAmount;
final int? totalCost;
final int? remainingAmount;
final String? paymentStatus;
final int? refundAmount;
final bool? isVoid;
final bool? isRefund;
final String? notes;
final Map<String, dynamic>? metadata;
final DateTime? createdAt;
final DateTime? updatedAt;
final List<OrderItem>? orderItems;
final List<Payment>? payments;
final int? totalPaid;
final int? paymentCount;
final String? splitType;
Order({
this.id,
this.orderNumber,
this.outletId,
this.userId,
this.tableNumber,
this.orderType,
this.status,
this.subtotal,
this.taxAmount,
this.discountAmount,
this.totalAmount,
this.totalCost,
this.remainingAmount,
this.paymentStatus,
this.refundAmount,
this.isVoid,
this.isRefund,
this.notes,
this.metadata,
this.createdAt,
this.updatedAt,
this.orderItems,
this.payments,
this.totalPaid,
this.paymentCount,
this.splitType,
});
factory Order.fromMap(Map<String, dynamic> map) {
return Order(
id: map['id'],
orderNumber: map['order_number'],
outletId: map['outlet_id'],
userId: map['user_id'],
tableNumber: map['table_number'],
orderType: map['order_type'],
status: map['status'],
subtotal: map['subtotal'],
taxAmount: map['tax_amount'],
discountAmount: map['discount_amount'],
totalAmount: map['total_amount'],
totalCost: map['total_cost'],
remainingAmount: map['remaining_amount'],
paymentStatus: map['payment_status'],
refundAmount: map['refund_amount'],
isVoid: map['is_void'],
isRefund: map['is_refund'],
notes: map['notes'],
metadata: map['metadata'] ?? {},
createdAt: DateTime.parse(map['created_at']),
updatedAt: DateTime.parse(map['updated_at']),
orderItems: map["order_items"] == null
? []
: List<OrderItem>.from(
map['order_items'].map((x) => OrderItem.fromMap(x))),
payments: map["payments"] == null
? []
: List<Payment>.from(map['payments'].map((x) => Payment.fromMap(x))),
totalPaid: map['total_paid'],
paymentCount: map['payment_count'],
splitType: map['split_type'] ?? "",
);
}
Map<String, dynamic> toMap() {
return {
'id': id,
'order_number': orderNumber,
'outlet_id': outletId,
'user_id': userId,
'table_number': tableNumber,
'order_type': orderType,
'status': status,
'subtotal': subtotal,
'tax_amount': taxAmount,
'discount_amount': discountAmount,
'total_amount': totalAmount,
'total_cost': totalCost,
'remaining_amount': remainingAmount,
'payment_status': paymentStatus,
'refund_amount': refundAmount,
'is_void': isVoid,
'is_refund': isRefund,
'notes': notes,
'metadata': metadata,
'created_at': createdAt?.toIso8601String(),
'updated_at': updatedAt?.toIso8601String(),
'order_items': orderItems?.map((x) => x.toMap()).toList(),
'payments': payments?.map((x) => x.toMap()).toList(),
'total_paid': totalPaid,
'payment_count': paymentCount,
'split_type': splitType,
};
}
}
class OrderItem {
String? id;
String? orderId;
String? productId;
String? productName;
String? productVariantId;
String? productVariantName;
int? quantity;
int? unitPrice;
int? totalPrice;
List<dynamic>? modifiers;
String? notes;
String? status;
DateTime? createdAt;
DateTime? updatedAt;
String? printerType;
int? paidQuantity;
OrderItem({
this.id,
this.orderId,
this.productId,
this.productName,
this.productVariantId,
this.productVariantName,
this.quantity,
this.unitPrice,
this.totalPrice,
this.modifiers,
this.notes,
this.status,
this.createdAt,
this.updatedAt,
this.printerType,
this.paidQuantity,
});
factory OrderItem.fromMap(Map<String, dynamic> map) {
return OrderItem(
id: map['id'],
orderId: map['order_id'],
productId: map['product_id'],
productName: map['product_name'],
productVariantId: map['product_variant_id'],
productVariantName: map['product_variant_name'],
quantity: map['quantity'],
unitPrice: map['unit_price'],
totalPrice: map['total_price'],
modifiers:
map['modifiers'] == null ? [] : List<dynamic>.from(map['modifiers']),
notes: map['notes'],
status: map['status'],
createdAt: DateTime.parse(map['created_at']),
updatedAt: DateTime.parse(map['updated_at']),
printerType: map['printer_type'],
paidQuantity: map['paid_quantity'],
);
}
Map<String, dynamic> toMap() {
return {
'id': id,
'order_id': orderId,
'product_id': productId,
'product_name': productName,
'product_variant_id': productVariantId,
'product_variant_name': productVariantName,
'quantity': quantity,
'unit_price': unitPrice,
'total_price': totalPrice,
'modifiers': modifiers,
'notes': notes,
'status': status,
'created_at': createdAt?.toIso8601String(),
'updated_at': updatedAt?.toIso8601String(),
'printer_type': printerType,
'paid_quantity': paidQuantity,
};
}
}
class Payment {
final String? id;
final String? orderId;
final String? paymentMethodId;
final String? paymentMethodName;
final String? paymentMethodType;
final int? amount;
final String? status;
final int? splitNumber;
final int? splitTotal;
final String? splitDescription;
final int? refundAmount;
final Map<String, dynamic>? metadata;
final DateTime? createdAt;
final DateTime? updatedAt;
Payment({
this.id,
this.orderId,
this.paymentMethodId,
this.paymentMethodName,
this.paymentMethodType,
this.amount,
this.status,
this.splitNumber,
this.splitTotal,
this.splitDescription,
this.refundAmount,
this.metadata,
this.createdAt,
this.updatedAt,
});
factory Payment.fromMap(Map<String, dynamic> map) {
return Payment(
id: map['id'],
orderId: map['order_id'],
paymentMethodId: map['payment_method_id'],
paymentMethodName: map['payment_method_name'],
paymentMethodType: map['payment_method_type'],
amount: map['amount'],
status: map['status'],
splitNumber: map['split_number'],
splitTotal: map['split_total'],
splitDescription: map['split_description'],
refundAmount: map['refund_amount'],
metadata: map['metadata'] ?? {},
createdAt: DateTime.parse(map['created_at']),
updatedAt: DateTime.parse(map['updated_at']),
);
}
Map<String, dynamic> toMap() {
return {
'id': id,
'order_id': orderId,
'payment_method_id': paymentMethodId,
'payment_method_name': paymentMethodName,
'payment_method_type': paymentMethodType,
'amount': amount,
'status': status,
'split_number': splitNumber,
'split_total': splitTotal,
'split_description': splitDescription,
'refund_amount': refundAmount,
'metadata': metadata,
'created_at': createdAt?.toIso8601String(),
'updated_at': updatedAt?.toIso8601String(),
};
}
}

View File

@ -0,0 +1,173 @@
class PaymentMethodAnalyticResponseModel {
final bool success;
final PaymentMethodAnalyticData data;
final dynamic errors;
PaymentMethodAnalyticResponseModel({
required this.success,
required this.data,
this.errors,
});
factory PaymentMethodAnalyticResponseModel.fromJson(
Map<String, dynamic> json) =>
PaymentMethodAnalyticResponseModel.fromMap(json);
Map<String, dynamic> toJson() => toMap();
factory PaymentMethodAnalyticResponseModel.fromMap(Map<String, dynamic> map) {
return PaymentMethodAnalyticResponseModel(
success: map['success'],
data: PaymentMethodAnalyticData.fromMap(map['data']),
errors: map['errors'],
);
}
Map<String, dynamic> toMap() {
return {
'success': success,
'data': data.toMap(),
'errors': errors,
};
}
}
class PaymentMethodAnalyticData {
final String organizationId;
final String outletId;
final DateTime dateFrom;
final DateTime dateTo;
final String groupBy;
final PaymentSummary summary;
final List<PaymentMethodAnalyticItem> data;
PaymentMethodAnalyticData({
required this.organizationId,
required this.outletId,
required this.dateFrom,
required this.dateTo,
required this.groupBy,
required this.summary,
required this.data,
});
factory PaymentMethodAnalyticData.fromJson(Map<String, dynamic> json) =>
PaymentMethodAnalyticData.fromMap(json);
Map<String, dynamic> toJson() => toMap();
factory PaymentMethodAnalyticData.fromMap(Map<String, dynamic> map) {
return PaymentMethodAnalyticData(
organizationId: map['organization_id'],
outletId: map['outlet_id'],
dateFrom: DateTime.parse(map['date_from']),
dateTo: DateTime.parse(map['date_to']),
groupBy: map['group_by'],
summary: PaymentSummary.fromMap(map['summary']),
data: map['data'] == null
? []
: List<PaymentMethodAnalyticItem>.from(
map['data']?.map((x) => PaymentMethodAnalyticItem.fromMap(x)) ??
[],
),
);
}
Map<String, dynamic> toMap() {
return {
'organization_id': organizationId,
'outlet_id': outletId,
'date_from': dateFrom.toIso8601String(),
'date_to': dateTo.toIso8601String(),
'group_by': groupBy,
'summary': summary.toMap(),
'data': data.map((x) => x.toMap()).toList(),
};
}
}
class PaymentSummary {
final int totalAmount;
final int totalOrders;
final int totalPayments;
final double averageOrderValue;
PaymentSummary({
required this.totalAmount,
required this.totalOrders,
required this.totalPayments,
required this.averageOrderValue,
});
factory PaymentSummary.fromJson(Map<String, dynamic> json) =>
PaymentSummary.fromMap(json);
Map<String, dynamic> toJson() => toMap();
factory PaymentSummary.fromMap(Map<String, dynamic> map) {
return PaymentSummary(
totalAmount: map['total_amount'],
totalOrders: map['total_orders'],
totalPayments: map['total_payments'],
averageOrderValue: (map['average_order_value'] as num).toDouble(),
);
}
Map<String, dynamic> toMap() {
return {
'total_amount': totalAmount,
'total_orders': totalOrders,
'total_payments': totalPayments,
'average_order_value': averageOrderValue,
};
}
}
class PaymentMethodAnalyticItem {
final String paymentMethodId;
final String paymentMethodName;
final String paymentMethodType;
final int totalAmount;
final int orderCount;
final int paymentCount;
final int percentage;
PaymentMethodAnalyticItem({
required this.paymentMethodId,
required this.paymentMethodName,
required this.paymentMethodType,
required this.totalAmount,
required this.orderCount,
required this.paymentCount,
required this.percentage,
});
factory PaymentMethodAnalyticItem.fromJson(Map<String, dynamic> json) =>
PaymentMethodAnalyticItem.fromMap(json);
Map<String, dynamic> toJson() => toMap();
factory PaymentMethodAnalyticItem.fromMap(Map<String, dynamic> map) {
return PaymentMethodAnalyticItem(
paymentMethodId: map['payment_method_id'],
paymentMethodName: map['payment_method_name'],
paymentMethodType: map['payment_method_type'],
totalAmount: map['total_amount'],
orderCount: map['order_count'],
paymentCount: map['payment_count'],
percentage: map['percentage'],
);
}
Map<String, dynamic> toMap() {
return {
'payment_method_id': paymentMethodId,
'payment_method_name': paymentMethodName,
'payment_method_type': paymentMethodType,
'total_amount': totalAmount,
'order_count': orderCount,
'payment_count': paymentCount,
'percentage': percentage,
};
}
}

View File

@ -1,12 +1,14 @@
import 'dart:convert'; import 'dart:convert';
class PaymentMethodsResponseModel { class PaymentMethodsResponseModel {
final String? status; final bool? success;
final List<PaymentMethod>? data; final PaymentMethodsData? data;
final dynamic errors;
PaymentMethodsResponseModel({ PaymentMethodsResponseModel({
this.status, this.success,
this.data, this.data,
this.errors,
}); });
factory PaymentMethodsResponseModel.fromJson(String str) => factory PaymentMethodsResponseModel.fromJson(String str) =>
@ -16,51 +18,83 @@ class PaymentMethodsResponseModel {
factory PaymentMethodsResponseModel.fromMap(Map<String, dynamic> json) => factory PaymentMethodsResponseModel.fromMap(Map<String, dynamic> json) =>
PaymentMethodsResponseModel( PaymentMethodsResponseModel(
status: json["status"], success: json["success"],
data: json["data"] == null data: json["data"] == null
? [] ? null
: List<PaymentMethod>.from( : PaymentMethodsData.fromMap(json["data"]),
json["data"]!.map((x) => PaymentMethod.fromMap(x))), errors: json["errors"],
); );
Map<String, dynamic> toMap() => { Map<String, dynamic> toMap() => {
"status": status, "success": success,
"data": data == null "data": data?.toMap(),
"errors": errors,
};
}
class PaymentMethodsData {
final List<PaymentMethod>? paymentMethods;
final int? totalCount;
final int? page;
final int? limit;
final int? totalPages;
PaymentMethodsData({
this.paymentMethods,
this.totalCount,
this.page,
this.limit,
this.totalPages,
});
factory PaymentMethodsData.fromMap(Map<String, dynamic> json) =>
PaymentMethodsData(
paymentMethods: json["payment_methods"] == null
? [] ? []
: List<dynamic>.from(data!.map((x) => x.toMap())), : List<PaymentMethod>.from(
json["payment_methods"].map((x) => PaymentMethod.fromMap(x))),
totalCount: json["total_count"],
page: json["page"],
limit: json["limit"],
totalPages: json["total_pages"],
);
Map<String, dynamic> toMap() => {
"payment_methods": paymentMethods == null
? []
: List<dynamic>.from(paymentMethods!.map((x) => x.toMap())),
"total_count": totalCount,
"page": page,
"limit": limit,
"total_pages": totalPages,
}; };
} }
class PaymentMethod { class PaymentMethod {
final int? id; final String? id;
final String? organizationId;
final String? name; final String? name;
final String? description; final String? type;
final bool? isActive; final bool? isActive;
final int? sortOrder;
final DateTime? createdAt; final DateTime? createdAt;
final DateTime? updatedAt; final DateTime? updatedAt;
PaymentMethod({ PaymentMethod({
this.id, this.id,
this.organizationId,
this.name, this.name,
this.description, this.type,
this.isActive, this.isActive,
this.sortOrder,
this.createdAt, this.createdAt,
this.updatedAt, this.updatedAt,
}); });
factory PaymentMethod.fromJson(String str) =>
PaymentMethod.fromMap(json.decode(str));
String toJson() => json.encode(toMap());
factory PaymentMethod.fromMap(Map<String, dynamic> json) => PaymentMethod( factory PaymentMethod.fromMap(Map<String, dynamic> json) => PaymentMethod(
id: json["id"], id: json["id"],
organizationId: json["organization_id"],
name: json["name"], name: json["name"],
description: json["description"], type: json["type"],
isActive: json["is_active"], isActive: json["is_active"],
sortOrder: json["sort_order"],
createdAt: json["created_at"] == null createdAt: json["created_at"] == null
? null ? null
: DateTime.parse(json["created_at"]), : DateTime.parse(json["created_at"]),
@ -71,11 +105,11 @@ class PaymentMethod {
Map<String, dynamic> toMap() => { Map<String, dynamic> toMap() => {
"id": id, "id": id,
"organization_id": organizationId,
"name": name, "name": name,
"description": description, "type": type,
"is_active": isActive, "is_active": isActive,
"sort_order": sortOrder,
"created_at": createdAt?.toIso8601String(), "created_at": createdAt?.toIso8601String(),
"updated_at": updatedAt?.toIso8601String(), "updated_at": updatedAt?.toIso8601String(),
}; };
} }

View File

@ -0,0 +1,95 @@
import 'dart:convert';
class PaymentSuccessResponseModel {
final bool? success;
final PaymentData? data;
final dynamic errors;
PaymentSuccessResponseModel({
this.success,
this.data,
this.errors,
});
factory PaymentSuccessResponseModel.fromJson(String str) =>
PaymentSuccessResponseModel.fromMap(json.decode(str));
String toJson() => json.encode(toMap());
factory PaymentSuccessResponseModel.fromMap(Map<String, dynamic> json) =>
PaymentSuccessResponseModel(
success: json["success"],
data: json["data"] == null ? null : PaymentData.fromMap(json["data"]),
errors: json["errors"],
);
Map<String, dynamic> toMap() => {
"success": success,
"data": data?.toMap(),
"errors": errors,
};
}
class PaymentData {
final String? id;
final String? orderId;
final String? paymentMethodId;
final int? amount;
final String? status;
final String? transactionId;
final int? splitNumber;
final int? splitTotal;
final String? splitDescription;
final int? refundAmount;
final DateTime? createdAt;
final DateTime? updatedAt;
PaymentData({
this.id,
this.orderId,
this.paymentMethodId,
this.amount,
this.status,
this.transactionId,
this.splitNumber,
this.splitTotal,
this.splitDescription,
this.refundAmount,
this.createdAt,
this.updatedAt,
});
factory PaymentData.fromMap(Map<String, dynamic> json) => PaymentData(
id: json["id"],
orderId: json["order_id"],
paymentMethodId: json["payment_method_id"],
amount: json["amount"],
status: json["status"],
transactionId: json["transaction_id"],
splitNumber: json["split_number"],
splitTotal: json["split_total"],
splitDescription: json["split_description"],
refundAmount: json["refund_amount"],
createdAt: json["created_at"] == null
? null
: DateTime.parse(json["created_at"]),
updatedAt: json["updated_at"] == null
? null
: DateTime.parse(json["updated_at"]),
);
Map<String, dynamic> toMap() => {
"id": id,
"order_id": orderId,
"payment_method_id": paymentMethodId,
"amount": amount,
"status": status,
"transaction_id": transactionId,
"split_number": splitNumber,
"split_total": splitTotal,
"split_description": splitDescription,
"refund_amount": refundAmount,
"created_at": createdAt?.toIso8601String(),
"updated_at": updatedAt?.toIso8601String(),
};
}

View File

@ -0,0 +1,145 @@
class ProductAnalyticResponseModel {
final bool success;
final ProductAnalyticData data;
final dynamic errors;
ProductAnalyticResponseModel({
required this.success,
required this.data,
this.errors,
});
factory ProductAnalyticResponseModel.fromJson(Map<String, dynamic> json) =>
ProductAnalyticResponseModel.fromMap(json);
Map<String, dynamic> toJson() => toMap();
factory ProductAnalyticResponseModel.fromMap(Map<String, dynamic> map) {
return ProductAnalyticResponseModel(
success: map['success'] ?? false,
data: ProductAnalyticData.fromMap(map['data']),
errors: map['errors'],
);
}
Map<String, dynamic> toMap() {
return {
'success': success,
'data': data.toMap(),
'errors': errors,
};
}
}
class ProductAnalyticData {
final String organizationId;
final String outletId;
final DateTime dateFrom;
final DateTime dateTo;
final List<ProductAnalyticItem> data;
ProductAnalyticData({
required this.organizationId,
required this.outletId,
required this.dateFrom,
required this.dateTo,
required this.data,
});
factory ProductAnalyticData.fromMap(Map<String, dynamic> map) =>
ProductAnalyticData(
organizationId: map['organization_id'],
outletId: map['outlet_id'],
dateFrom: DateTime.parse(map['date_from']),
dateTo: DateTime.parse(map['date_to']),
data: map['data'] == null
? []
: List<ProductAnalyticItem>.from(
map['data'].map((x) => ProductAnalyticItem.fromMap(x)),
),
);
Map<String, dynamic> toMap() => {
'organization_id': organizationId,
'outlet_id': outletId,
'date_from': dateFrom.toIso8601String(),
'date_to': dateTo.toIso8601String(),
'data': data.map((x) => x.toMap()).toList(),
};
}
class ProductAnalyticItem {
final String productId;
final String productName;
final String categoryId;
final String categoryName;
final int quantitySold;
final int revenue;
final double averagePrice;
final int orderCount;
ProductAnalyticItem({
required this.productId,
required this.productName,
required this.categoryId,
required this.categoryName,
required this.quantitySold,
required this.revenue,
required this.averagePrice,
required this.orderCount,
});
factory ProductAnalyticItem.fromMap(Map<String, dynamic> map) =>
ProductAnalyticItem(
productId: map['product_id'],
productName: map['product_name'],
categoryId: map['category_id'],
categoryName: map['category_name'],
quantitySold: map['quantity_sold'],
revenue: map['revenue'],
averagePrice: (map['average_price'] as num).toDouble(),
orderCount: map['order_count'],
);
Map<String, dynamic> toMap() => {
'product_id': productId,
'product_name': productName,
'category_id': categoryId,
'category_name': categoryName,
'quantity_sold': quantitySold,
'revenue': revenue,
'average_price': averagePrice,
'order_count': orderCount,
};
}
class ProductInsights {
final List<ProductAnalyticItem> topProducts;
final ProductAnalyticItem? bestProduct;
final List<CategorySummary> categorySummary;
final int totalProducts;
final int totalRevenue;
final int totalQuantitySold;
ProductInsights({
required this.topProducts,
required this.bestProduct,
required this.categorySummary,
required this.totalProducts,
required this.totalRevenue,
required this.totalQuantitySold,
});
}
// Category summary class
class CategorySummary {
final String categoryName;
int productCount;
int totalRevenue;
CategorySummary({
required this.categoryName,
required this.productCount,
required this.totalRevenue,
});
}

View File

@ -1,17 +1,15 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first // ignore_for_file: public_member_api_docs, sort_constructors_first
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:enaklo_pos/presentation/home/pages/confirm_payment_page.dart';
class ProductResponseModel { class ProductResponseModel {
final String? status; final bool? success;
final List<Product>? data; final ProductData? data;
final dynamic errors;
ProductResponseModel({ ProductResponseModel({
this.status, this.success,
this.data, this.data,
this.errors,
}); });
factory ProductResponseModel.fromJson(String str) => factory ProductResponseModel.fromJson(String str) =>
@ -21,50 +19,90 @@ class ProductResponseModel {
factory ProductResponseModel.fromMap(Map<String, dynamic> json) => factory ProductResponseModel.fromMap(Map<String, dynamic> json) =>
ProductResponseModel( ProductResponseModel(
status: json["status"], success: json["success"],
data: json["data"] == null data: json["data"] == null ? null : ProductData.fromMap(json["data"]),
? [] errors: json["errors"],
: List<Product>.from(json["data"]!.map((x) => Product.fromMap(x))),
); );
Map<String, dynamic> toMap() => { Map<String, dynamic> toMap() => {
"status": status, "success": success,
"data": "data": data?.toMap(),
data == null ? [] : List<dynamic>.from(data!.map((x) => x.toMap())), "errors": errors,
};
}
class ProductData {
final List<Product>? products;
final int? totalCount;
final int? page;
final int? limit;
final int? totalPages;
ProductData({
this.products,
this.totalCount,
this.page,
this.limit,
this.totalPages,
});
factory ProductData.fromMap(Map<String, dynamic> json) => ProductData(
products: json["products"] == null
? []
: List<Product>.from(
json["products"].map((x) => Product.fromMap(x))),
totalCount: json["total_count"],
page: json["page"],
limit: json["limit"],
totalPages: json["total_pages"],
);
Map<String, dynamic> toMap() => {
"products": products == null
? []
: List<dynamic>.from(products!.map((x) => x.toMap())),
"total_count": totalCount,
"page": page,
"limit": limit,
"total_pages": totalPages,
}; };
} }
class Product { class Product {
final int? id; final String? id;
final int? productId; final String? organizationId;
final int? categoryId; final String? categoryId;
final String? sku;
final String? name; final String? name;
final String? description; final String? description;
final String? image; final int? price;
final String? price; final int? cost;
final int? stock; final String? businessType;
final int? status; final String? imageUrl;
final int? isFavorite; final String? printerType;
final Map<String, dynamic>? metadata;
final bool? isActive;
final DateTime? createdAt; final DateTime? createdAt;
final DateTime? updatedAt; final DateTime? updatedAt;
final Category? category; final List<ProductVariant>? variants;
final String? printerType;
Product({ Product({
this.id, this.id,
this.productId, this.organizationId,
this.categoryId, this.categoryId,
this.sku,
this.name, this.name,
this.description, this.description,
this.image,
this.price, this.price,
this.stock, this.cost,
this.status, this.businessType,
this.isFavorite, this.imageUrl,
this.printerType,
this.metadata,
this.isActive,
this.createdAt, this.createdAt,
this.updatedAt, this.updatedAt,
this.category, this.variants,
this.printerType,
}); });
factory Product.fromJson(String str) => Product.fromMap(json.decode(str)); factory Product.fromJson(String str) => Product.fromMap(json.decode(str));
@ -72,91 +110,102 @@ class Product {
String toJson() => json.encode(toMap()); String toJson() => json.encode(toMap());
factory Product.fromMap(Map<String, dynamic> json) => Product( factory Product.fromMap(Map<String, dynamic> json) => Product(
id: json["id"] is String ? int.tryParse(json["id"]) : json["id"], id: json["id"],
productId: json["product_id"] is String ? int.tryParse(json["product_id"]) : json["product_id"], organizationId: json["organization_id"],
categoryId: json["category_id"] is String categoryId: json["category_id"],
? int.tryParse(json["category_id"]) sku: json["sku"],
: json["category_id"],
name: json["name"], name: json["name"],
description: json["description"], description: json["description"],
image: json["image"], price: json["price"],
// price: json["price"].substring(0, json["price"].length - 3), cost: json["cost"],
price: json["price"].toString().replaceAll('.00', ''), businessType: json["business_type"],
stock: json["stock"] is String ? int.tryParse(json["stock"]) : json["stock"], imageUrl: json["image_url"],
status: json["status"] is String ? int.tryParse(json["status"]) : json["status"], printerType: json["printer_type"],
isFavorite: json["is_favorite"] is String ? int.tryParse(json["is_favorite"]) : json["is_favorite"], metadata: json["metadata"] ?? {},
isActive: json["is_active"],
createdAt: json["created_at"] == null createdAt: json["created_at"] == null
? null ? null
: DateTime.parse(json["created_at"]), : DateTime.parse(json["created_at"]),
updatedAt: json["updated_at"] == null updatedAt: json["updated_at"] == null
? null ? null
: DateTime.parse(json["updated_at"]), : DateTime.parse(json["updated_at"]),
category: json["category"] == null variants: json["variants"] == null
? null ? []
: Category.fromMap(json["category"]), : List<ProductVariant>.from(
printerType: json["printer_type"] ?? 'bar', json["variants"].map((x) => ProductVariant.fromMap(x))),
); );
factory Product.fromOrderMap(Map<String, dynamic> json) => Product( factory Product.fromOrderMap(Map<String, dynamic> json) => Product(
id: json["id_product"], id: json["id_product"],
price: json["price"].toString(), price: json["price"],
); );
factory Product.fromLocalMap(Map<String, dynamic> json) => Product( factory Product.fromLocalMap(Map<String, dynamic> json) => Product(
id: json["id"], id: json["id"],
productId: json["product_id"], organizationId: json["organization_id"],
categoryId: json["categoryId"], categoryId: json["category_id"],
category: Category( sku: json["sku"],
id: json["categoryId"],
name: json["categoryName"],
),
name: json["name"], name: json["name"],
description: json["description"], description: json["description"],
image: json["image"],
price: json["price"], price: json["price"],
stock: json["stock"], cost: json["cost"],
status: json["status"], businessType: json["business_type"],
isFavorite: json["isFavorite"], imageUrl: json["image_url"],
createdAt: json["createdAt"] == null printerType: json["printer_type"],
metadata: json["metadata"] ?? {},
isActive: json["is_active"],
createdAt: json["created_at"] == null
? null ? null
: DateTime.parse(json["createdAt"]), : DateTime.parse(json["created_at"]),
updatedAt: json["updatedAt"] == null updatedAt: json["updated_at"] == null
? null ? null
: DateTime.parse(json["updatedAt"]), : DateTime.parse(json["updated_at"]),
printerType: json["printer_type"] ?? 'bar', variants: json["variants"] == null
? []
: List<ProductVariant>.from(
json["variants"].map((x) => ProductVariant.fromMap(x))),
); );
Map<String, dynamic> toLocalMap() => { Map<String, dynamic> toLocalMap() => {
"product_id": id, "id": id,
"categoryId": categoryId, "organization_id": organizationId,
"categoryName": category?.name, "category_id": categoryId,
"sku": sku,
"name": name, "name": name,
"description": description, "description": description,
"image": image, "price": price,
"price": price?.replaceAll(RegExp(r'\.0+$'), ''), "cost": cost,
"stock": stock, "business_type": businessType,
"status": status, "image_url": imageUrl,
"isFavorite": isFavorite,
"createdAt": createdAt?.toIso8601String(),
"updatedAt": updatedAt?.toIso8601String(),
"printer_type": printerType, "printer_type": printerType,
"metadata": metadata,
"is_active": isActive,
"created_at": createdAt?.toIso8601String(),
"updated_at": updatedAt?.toIso8601String(),
"variants": variants == null
? []
: List<dynamic>.from(variants!.map((x) => x.toMap())),
}; };
Map<String, dynamic> toMap() => { Map<String, dynamic> toMap() => {
"id": id, "id": id,
"product_id": productId, "organization_id": organizationId,
"category_id": categoryId, "category_id": categoryId,
"sku": sku,
"name": name, "name": name,
"description": description, "description": description,
"image": image,
"price": price, "price": price,
"stock": stock, "cost": cost,
"status": status, "business_type": businessType,
"is_favorite": isFavorite, "image_url": imageUrl,
"printer_type": printerType,
"metadata": metadata,
"is_active": isActive,
"created_at": createdAt?.toIso8601String(), "created_at": createdAt?.toIso8601String(),
"updated_at": updatedAt?.toIso8601String(), "updated_at": updatedAt?.toIso8601String(),
"category": category?.toMap(), "variants": variants == null
"printer_type": printerType, ? []
: List<dynamic>.from(variants!.map((x) => x.toMap())),
}; };
@override @override
@ -164,70 +213,88 @@ class Product {
if (identical(this, other)) return true; if (identical(this, other)) return true;
return other.id == id && return other.id == id &&
other.productId == productId && other.organizationId == organizationId &&
other.categoryId == categoryId && other.categoryId == categoryId &&
other.sku == sku &&
other.name == name && other.name == name &&
other.description == description && other.description == description &&
other.image == image &&
other.price == price && other.price == price &&
other.stock == stock && other.cost == cost &&
other.status == status && other.businessType == businessType &&
other.isFavorite == isFavorite && other.imageUrl == imageUrl &&
other.printerType == printerType &&
other.metadata == metadata &&
other.isActive == isActive &&
other.createdAt == createdAt && other.createdAt == createdAt &&
other.updatedAt == updatedAt && other.updatedAt == updatedAt &&
other.category == category && _listEquals(other.variants, variants);
other.printerType == printerType; }
bool _listEquals(List<ProductVariant>? a, List<ProductVariant>? b) {
if (a == null && b == null) return true;
if (a == null || b == null) return false;
if (a.length != b.length) return false;
for (int i = 0; i < a.length; i++) {
if (a[i] != b[i]) return false;
}
return true;
} }
@override @override
int get hashCode { int get hashCode {
return id.hashCode ^ return id.hashCode ^
productId.hashCode ^ organizationId.hashCode ^
categoryId.hashCode ^ categoryId.hashCode ^
sku.hashCode ^
name.hashCode ^ name.hashCode ^
description.hashCode ^ description.hashCode ^
image.hashCode ^
price.hashCode ^ price.hashCode ^
stock.hashCode ^ cost.hashCode ^
status.hashCode ^ businessType.hashCode ^
isFavorite.hashCode ^ imageUrl.hashCode ^
printerType.hashCode ^
metadata.hashCode ^
isActive.hashCode ^
createdAt.hashCode ^ createdAt.hashCode ^
updatedAt.hashCode ^ updatedAt.hashCode ^
category.hashCode ^ variants.hashCode;
printerType.hashCode;
} }
Product copyWith({ Product copyWith({
int? id, String? id,
int? productId, String? organizationId,
int? categoryId, String? categoryId,
String? sku,
String? name, String? name,
String? description, String? description,
String? image, int? price,
String? price, int? cost,
int? stock, String? businessType,
int? status, String? imageUrl,
int? isFavorite, String? printerType,
Map<String, dynamic>? metadata,
bool? isActive,
DateTime? createdAt, DateTime? createdAt,
DateTime? updatedAt, DateTime? updatedAt,
Category? category, List<ProductVariant>? variants,
String? printerType,
}) { }) {
return Product( return Product(
id: id ?? this.id, id: id ?? this.id,
productId: productId ?? this.productId, organizationId: organizationId ?? this.organizationId,
categoryId: categoryId ?? this.categoryId, categoryId: categoryId ?? this.categoryId,
sku: sku ?? this.sku,
name: name ?? this.name, name: name ?? this.name,
description: description ?? this.description, description: description ?? this.description,
image: image ?? this.image,
price: price ?? this.price, price: price ?? this.price,
stock: stock ?? this.stock, cost: cost ?? this.cost,
status: status ?? this.status, businessType: businessType ?? this.businessType,
isFavorite: isFavorite ?? this.isFavorite, imageUrl: imageUrl ?? this.imageUrl,
printerType: printerType ?? this.printerType,
metadata: metadata ?? this.metadata,
isActive: isActive ?? this.isActive,
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt, updatedAt: updatedAt ?? this.updatedAt,
category: category ?? this.category, variants: variants ?? this.variants,
printerType: printerType ?? this.printerType,
); );
} }
} }
@ -297,3 +364,51 @@ class Category {
updatedAt.hashCode; updatedAt.hashCode;
} }
} }
class ProductVariant {
final String? id;
final String? productId;
final String? name;
final int? priceModifier;
final int? cost;
final Map<String, dynamic>? metadata;
final DateTime? createdAt;
final DateTime? updatedAt;
ProductVariant({
this.id,
this.productId,
this.name,
this.priceModifier,
this.cost,
this.metadata,
this.createdAt,
this.updatedAt,
});
factory ProductVariant.fromMap(Map<String, dynamic> json) => ProductVariant(
id: json["id"],
productId: json["product_id"],
name: json["name"],
priceModifier: json["price_modifier"],
cost: json["cost"],
metadata: json["metadata"] ?? {},
createdAt: json["created_at"] == null
? null
: DateTime.parse(json["created_at"]),
updatedAt: json["updated_at"] == null
? null
: DateTime.parse(json["updated_at"]),
);
Map<String, dynamic> toMap() => {
"id": id,
"product_id": productId,
"name": name,
"price_modifier": priceModifier,
"cost": cost,
"metadata": metadata,
"created_at": createdAt?.toIso8601String(),
"updated_at": updatedAt?.toIso8601String(),
};
}

Some files were not shown because too many files have changed in this diff Show More