Compare commits

..
64 Commits
Author SHA1 Message Date
Efril 457ed38827 fix print cut 2026-03-01 12:28:00 +07:00
Efril d34487d883 update version 2026-02-28 13:20:13 +07:00
Efril f4cdfcb96f fix print 2026-02-27 19:09:39 +07:00
Efril 0bccec8a1e fix printer 2026-02-24 00:10:02 +07:00
efrilm 86d2196a04 version 2026-01-14 10:33:53 +07:00
efrilm aa25de8da7 void product 2025-11-21 20:03:06 +07:00
efrilm 96387c08f4 fix split bill 2025-11-21 19:53:02 +07:00
efrilm e585cf4292 update 2025-10-23 20:11:16 +07:00
efrilm 290360674f update 2025-10-08 17:06:23 +07:00
efrilm 1fbacae1f4 fix category tabbar 2025-10-08 13:01:26 +07:00
efrilm 613b216c04 fix force close 2025-10-07 21:49:15 +07:00
efrilm 455a6afd70 print kitchen update 2025-10-02 17:57:48 +07:00
efrilm 2813011fac update print 2025-09-27 18:10:47 +07:00
efrilm 83af323a2f version and build number 2025-09-22 15:11:04 +07:00
efrilm cef1f79032 fix overflow in confirm payment 2025-09-22 14:47:41 +07:00
efrilm 59a8d7f661 refresh token 2025-09-20 18:05:15 +07:00
efrilm a58d1040af sync data 2025-09-20 05:02:01 +07:00
efrilm 72a464b4c0 data sync page 2025-09-20 04:45:08 +07:00
efrilm 5b980d237f category local 2025-09-20 04:36:22 +07:00
efrilm c12d6525fa Update 2025-09-20 03:57:12 +07:00
efrilm 44402140fb Printer Local data and remove unused 2025-09-20 03:56:07 +07:00
efrilm 04811015b6 data sync overflow 2025-09-20 03:12:32 +07:00
efrilm f104390141 sync product to local 2025-09-20 03:10:05 +07:00
efrilm 3022d8de9f null safety 2025-09-19 19:12:35 +07:00
efrilm 6acfbdc4ca print on sales 2025-09-19 18:49:55 +07:00
efrilm 7b12793618 print 2025-09-19 18:23:55 +07:00
efrilm a6ac12c36a tracking print 2025-09-19 13:52:29 +07:00
efrilm e1e702bd76 fix null 2025-09-19 13:31:17 +07:00
efrilm fabb77d2b9 fix home error 2025-09-19 13:18:11 +07:00
efrilm f18a70b312 fix force close at home 2025-09-19 13:01:28 +07:00
efrilm 78914d7140 update 2025-09-18 21:47:34 +07:00
efrilm 937b98e45b update 2025-09-18 21:36:32 +07:00
efrilm c1be16845e update 2025-09-18 21:22:40 +07:00
efrilm dcb929cde5 printer setting null safety 2025-09-17 22:43:46 +07:00
efrilm 8aa326c513 printer setting null safety 2025-09-17 22:35:00 +07:00
efrilm bd02f389e5 table number null safety 2025-09-17 22:22:55 +07:00
efrilm 5534aad0d4 fix: product 2025-09-03 12:15:43 +07:00
efrilm 6599e6fe7c fear: rev 2 2025-09-02 19:49:01 +07:00
efrilm 3624f75bea feat: print void 2025-09-01 22:36:26 +07:00
efrilm 0d91d43866 fix: print split bill 2025-09-01 22:25:15 +07:00
efrilm dcd739e4a4 fix: checkout items 2025-09-01 22:12:56 +07:00
efrilm 631ac4bc46 feat: update 2025-08-15 17:08:17 +07:00
efrilm 7007f39766 feat: category report 2025-08-15 17:07:29 +07:00
efrilm 79585b253d feat: update 2025-08-15 16:49:24 +07:00
efrilm cc8012354f feat: date range picker 2025-08-15 16:09:25 +07:00
efrilm 2c75fcf582 feat: inventory report pdf 2025-08-15 01:36:50 +07:00
efrilm 34c0ad5411 feat: inventory report 2025-08-15 01:12:04 +07:00
efrilm f37814fec8 feat: report transaction 2025-08-14 21:19:34 +07:00
efrilm 0c375de6e4 fix: report profit loss 2025-08-14 17:32:16 +07:00
efrilm f87309af3f feat: update 2025-08-14 00:16:42 +07:00
efrilm 96d0b91984 feat: succes payment 2025-08-14 00:12:49 +07:00
efrilm e20dc53fb8 feat: profit loss widget 2025-08-13 23:27:52 +07:00
efrilm c09822b470 feat: success save order 2025-08-13 23:08:12 +07:00
efrilm 6a9a26f71c fix: success save order 2025-08-13 23:00:11 +07:00
efrilm b0006ee6bc fix: order and add to order 2025-08-13 22:52:37 +07:00
efrilm b34428965e fix: bug 2025-08-13 22:11:24 +07:00
efrilm 670a0b6414 feat: update split bill 2025-08-08 23:04:35 +07:00
efrilm b7ab9c6939 feat: update logo 2025-08-08 19:44:00 +07:00
efrilm 6b5af82307 feat: table 2025-08-08 14:59:49 +07:00
efrilm 2848678cdc fix: amount 2025-08-08 14:39:56 +07:00
efrilm f05347c90d feat: transfer table 2025-08-08 14:22:12 +07:00
efrilm 94725964ea fix: change position table 2025-08-08 13:45:03 +07:00
efrilm 2eceb0bc0c feat: update 2025-08-08 10:53:46 +07:00
efrilm e19d788f47 feat: update order detail 2025-08-08 10:29:17 +07:00
206 changed files with 19680 additions and 8589 deletions
+5 -1
View File
@@ -2,6 +2,8 @@ plugins {
id "com.android.application" id "com.android.application"
id "kotlin-android" id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin" id "dev.flutter.flutter-gradle-plugin"
id "com.google.gms.google-services"
id "com.google.firebase.crashlytics"
} }
def localProperties = new Properties() def localProperties = new Properties()
@@ -45,7 +47,7 @@ android {
applicationId "com.appscale.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 flutter.minSdkVersion
targetSdkVersion 35 targetSdkVersion 35
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
@@ -69,4 +71,6 @@ dependencies {
//multiDex //multiDex
implementation 'com.android.support:multidex:1.0.3' implementation 'com.android.support:multidex:1.0.3'
implementation 'androidx.multidex:multidex:2.0.1' implementation 'androidx.multidex:multidex:2.0.1'
implementation platform('com.google.firebase:firebase-bom:34.2.0')
implementation 'com.google.firebase:firebase-crashlytics'
} }
+29
View File
@@ -0,0 +1,29 @@
{
"project_info": {
"project_number": "1094037575589",
"project_id": "apskel-pos",
"storage_bucket": "apskel-pos.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:1094037575589:android:5157684038a971a3d3fa4c",
"android_client_info": {
"package_name": "com.appscale.pos"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyAle1ZcO4Z7wtNJUHkHEVkuBHr6mHcvPvI"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 983 B

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 19 KiB

+2
View File
@@ -7,6 +7,8 @@ buildscript {
dependencies { dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.google.gms:google-services:4.4.3'
classpath 'com.google.firebase:firebase-crashlytics-gradle:3.0.6'
} }
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 320 B

After

Width:  |  Height:  |  Size: 666 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 804 B

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 492 B

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 804 B

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 15 KiB

+4 -4
View File
@@ -3,15 +3,15 @@
flutter_launcher_icons: flutter_launcher_icons:
android: "launcher_icon" android: "launcher_icon"
ios: true ios: true
image_path: "assets/logo/ic_launcher.png" image_path: "assets/logo/logo.png"
remove_alpha_ios: true remove_alpha_ios: true
min_sdk_android: 21 # android min sdk min:16, default 21 min_sdk_android: 21 # android min sdk min:16, default 21
adaptive_icon_background: "#ffffff" adaptive_icon_background: "#ffffff"
adaptive_icon_foreground: "assets/logo/ic_launcher.png" adaptive_icon_foreground: "assets/logo/logo.png"
web: web:
generate: true generate: true
image_path: "assets/logo/ic_launcher.png" image_path: "assets/logo/logo.png"
windows: windows:
generate: true generate: true
image_path: "assets/logo/ic_launcher.png" image_path: "assets/logo/logo.png"
icon_size: 48 icon_size: 48
+4 -17
View File
@@ -304,12 +304,8 @@ class $AssetsImagesGen {
class $AssetsLogoGen { class $AssetsLogoGen {
const $AssetsLogoGen(); const $AssetsLogoGen();
/// File path: assets/logo/ic_launcher.png /// File path: assets/logo/logo.png
AssetGenImage get icLauncher => AssetGenImage get logo => const AssetGenImage('assets/logo/logo.png');
const AssetGenImage('assets/logo/ic_launcher.png');
/// File path: assets/logo/logo.jpeg
AssetGenImage get logo => const AssetGenImage('assets/logo/logo.jpeg');
/// File path: assets/logo/logo2.png /// File path: assets/logo/logo2.png
AssetGenImage get logo2 => const AssetGenImage('assets/logo/logo2.png'); AssetGenImage get logo2 => const AssetGenImage('assets/logo/logo2.png');
@@ -334,17 +330,8 @@ class $AssetsLogoGen {
AssetGenImage get mylogo => const AssetGenImage('assets/logo/mylogo.png'); AssetGenImage get mylogo => const AssetGenImage('assets/logo/mylogo.png');
/// List of all assets /// List of all assets
List<AssetGenImage> get values => [ List<AssetGenImage> get values =>
icLauncher, [logo, logo2, logo3, logo4, logo5, logo6, logoAppIcon, mylogo];
logo,
logo2,
logo3,
logo4,
logo5,
logo6,
logoAppIcon,
mylogo
];
} }
class Assets { class Assets {
+2 -1
View File
@@ -70,7 +70,8 @@ class _CustomDatePickerState extends State<CustomDatePicker> {
), ),
decoration: InputDecoration( decoration: InputDecoration(
suffixIcon: Padding( suffixIcon: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.symmetric(
vertical: 8, horizontal: 16),
child: Assets.icons.calendar.svg(), child: Assets.icons.calendar.svg(),
), ),
prefix: widget.prefix, prefix: widget.prefix,
+409
View File
@@ -0,0 +1,409 @@
import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_datepicker/datepicker.dart';
class DateRangePickerModal {
static Future<DateRangePickerSelectionChangedArgs?> show({
required BuildContext context,
String title = 'Pilih Rentang Tanggal',
DateTime? initialStartDate,
DateTime? initialEndDate,
DateTime? minDate,
DateTime? maxDate,
String confirmText = 'Pilih',
String cancelText = 'Batal',
Color primaryColor = Colors.blue,
Function(DateTime? startDate, DateTime? endDate)? onChanged,
}) async {
return await showDialog<DateRangePickerSelectionChangedArgs?>(
context: context,
barrierDismissible: false,
builder: (BuildContext context) => _DateRangePickerDialog(
title: title,
initialStartDate: initialStartDate,
initialEndDate: initialEndDate,
minDate: minDate,
maxDate: maxDate,
confirmText: confirmText,
cancelText: cancelText,
primaryColor: primaryColor,
onChanged: onChanged,
),
);
}
}
class _DateRangePickerDialog extends StatefulWidget {
final String title;
final DateTime? initialStartDate;
final DateTime? initialEndDate;
final DateTime? minDate;
final DateTime? maxDate;
final String confirmText;
final String cancelText;
final Color primaryColor;
final Function(DateTime? startDate, DateTime? endDate)? onChanged;
const _DateRangePickerDialog({
required this.title,
this.initialStartDate,
this.initialEndDate,
this.minDate,
this.maxDate,
required this.confirmText,
required this.cancelText,
required this.primaryColor,
this.onChanged,
});
@override
State<_DateRangePickerDialog> createState() => _DateRangePickerDialogState();
}
class _DateRangePickerDialogState extends State<_DateRangePickerDialog>
with TickerProviderStateMixin {
DateRangePickerSelectionChangedArgs? _selectionChangedArgs;
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
late Animation<double> _fadeAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_scaleAnimation = Tween<double>(
begin: 0.8,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.elasticOut,
));
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
_animationController.forward();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
void _onSelectionChanged(DateRangePickerSelectionChangedArgs args) {
setState(() {
_selectionChangedArgs = args;
});
// Note: onChanged callback is now called only when confirm button is pressed
// This allows users to see real-time selection without triggering callbacks
}
String _getSelectionText() {
if (_selectionChangedArgs?.value is PickerDateRange) {
final PickerDateRange range = _selectionChangedArgs!.value;
if (range.startDate != null && range.endDate != null) {
return '${_formatDate(range.startDate!)} - ${_formatDate(range.endDate!)}';
} else if (range.startDate != null) {
return _formatDate(range.startDate!);
}
}
return 'Belum ada tanggal dipilih';
}
String _formatDate(DateTime date) {
final months = [
'Jan',
'Feb',
'Mar',
'Apr',
'Mei',
'Jun',
'Jul',
'Agu',
'Sep',
'Okt',
'Nov',
'Des'
];
return '${date.day} ${months[date.month - 1]} ${date.year}';
}
bool get _isValidSelection {
if (_selectionChangedArgs?.value is PickerDateRange) {
final PickerDateRange range = _selectionChangedArgs!.value;
return range.startDate != null && range.endDate != null;
}
return false;
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return FadeTransition(
opacity: _fadeAnimation,
child: ScaleTransition(
scale: _scaleAnimation,
child: Dialog(
backgroundColor: Colors.transparent,
elevation: 0,
insetPadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
child: Container(
width: MediaQuery.of(context).size.width,
constraints: BoxConstraints(
maxWidth: 400,
maxHeight: MediaQuery.of(context).size.height * 0.85,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Header
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
widget.primaryColor,
widget.primaryColor.withOpacity(0.8),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
),
child: Row(
children: [
Icon(
Icons.calendar_today_rounded,
color: Colors.white,
size: 24,
),
const SizedBox(width: 12),
Expanded(
child: Text(
widget.title,
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
// Scrollable Content
Flexible(
child: SingleChildScrollView(
child: Column(
children: [
// Selection Info
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: widget.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: widget.primaryColor.withOpacity(0.2),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Tanggal Terpilih:',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: widget.primaryColor,
),
),
const SizedBox(height: 4),
Text(
_getSelectionText(),
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
],
),
),
// Date Picker
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16),
child: Container(
height: 320,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.grey.withOpacity(0.2),
),
),
child: SfDateRangePicker(
onSelectionChanged: _onSelectionChanged,
selectionMode:
DateRangePickerSelectionMode.range,
initialSelectedRange:
(widget.initialStartDate != null &&
widget.initialEndDate != null)
? PickerDateRange(
widget.initialStartDate,
widget.initialEndDate,
)
: null,
minDate: widget.minDate,
maxDate: widget.maxDate,
startRangeSelectionColor: widget.primaryColor,
endRangeSelectionColor: widget.primaryColor,
rangeSelectionColor:
widget.primaryColor.withOpacity(0.2),
todayHighlightColor: widget.primaryColor,
headerStyle: DateRangePickerHeaderStyle(
backgroundColor: Colors.transparent,
textAlign: TextAlign.center,
textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
monthViewSettings:
DateRangePickerMonthViewSettings(
viewHeaderStyle:
DateRangePickerViewHeaderStyle(
backgroundColor:
Colors.grey.withOpacity(0.1),
textStyle: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: widget.primaryColor,
),
),
),
selectionTextStyle: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 14,
),
rangeTextStyle: TextStyle(
color: widget.primaryColor,
fontWeight: FontWeight.w500,
fontSize: 14,
),
),
),
),
],
),
),
),
// Action Buttons
Padding(
padding: const EdgeInsets.all(20),
child: Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => Navigator.of(context).pop(),
style: OutlinedButton.styleFrom(
padding:
const EdgeInsets.symmetric(vertical: 14),
side: BorderSide(color: Colors.grey.shade400),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: Text(
widget.cancelText,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.grey,
),
),
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton(
onPressed: _isValidSelection
? () {
// Call onChanged when confirm button is pressed
if (widget.onChanged != null &&
_selectionChangedArgs?.value
is PickerDateRange) {
final PickerDateRange range =
_selectionChangedArgs!.value;
widget.onChanged!(
range.startDate, range.endDate);
}
Navigator.of(context)
.pop(_selectionChangedArgs);
}
: null,
style: ElevatedButton.styleFrom(
backgroundColor: widget.primaryColor,
padding:
const EdgeInsets.symmetric(vertical: 14),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
disabledBackgroundColor: Colors.grey.shade300,
),
child: Text(
widget.confirmText,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: _isValidSelection
? Colors.white
: Colors.grey.shade600,
),
),
),
),
],
),
),
],
),
),
),
),
);
},
);
}
}
+1 -1
View File
@@ -2,6 +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://enaklo-pos-be.altru.id'; static const String baseUrl = 'https://api-pos.apskel.id';
static const int defaultLimit = 10; static const int defaultLimit = 10;
} }
+161
View File
@@ -0,0 +1,161 @@
import 'dart:async';
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
class DatabaseHelper {
static DatabaseHelper? _instance;
static Database? _database;
DatabaseHelper._internal();
static DatabaseHelper get instance {
_instance ??= DatabaseHelper._internal();
return _instance!;
}
Future<Database> get database async {
_database ??= await _initDatabase();
return _database!;
}
Future<Database> _initDatabase() async {
String path = join(await getDatabasesPath(), 'pos_database.db');
return await openDatabase(
path,
version: 3, // Updated version for categories table
onCreate: _onCreate,
onUpgrade: _onUpgrade,
);
}
Future<void> _onCreate(Database db, int version) async {
// Products table
await db.execute('''
CREATE TABLE products (
id TEXT PRIMARY KEY,
organization_id TEXT,
category_id TEXT,
sku TEXT,
name TEXT,
description TEXT,
price INTEGER,
cost INTEGER,
business_type TEXT,
image_url TEXT,
printer_type TEXT,
metadata TEXT,
is_active INTEGER,
created_at TEXT,
updated_at TEXT
)
''');
// Product Variants table
await db.execute('''
CREATE TABLE product_variants (
id TEXT PRIMARY KEY,
product_id TEXT,
name TEXT,
price_modifier INTEGER,
cost INTEGER,
metadata TEXT,
created_at TEXT,
updated_at TEXT,
FOREIGN KEY (product_id) REFERENCES products (id) ON DELETE CASCADE
)
''');
// Categories table - NEW
await db.execute('''
CREATE TABLE categories (
id TEXT PRIMARY KEY,
organization_id TEXT,
name TEXT NOT NULL,
description TEXT,
business_type TEXT,
metadata TEXT,
is_active INTEGER DEFAULT 1,
created_at TEXT,
updated_at TEXT
)
''');
// Printer table
await db.execute('''
CREATE TABLE printers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
address TEXT,
paper TEXT,
type TEXT,
created_at TEXT,
updated_at TEXT
)
''');
// Create indexes for better performance
await db.execute(
'CREATE INDEX idx_products_category_id ON products(category_id)');
await db.execute('CREATE INDEX idx_products_name ON products(name)');
await db.execute('CREATE INDEX idx_products_sku ON products(sku)');
await db.execute('CREATE INDEX idx_categories_name ON categories(name)');
await db.execute(
'CREATE INDEX idx_categories_organization_id ON categories(organization_id)');
await db.execute(
'CREATE INDEX idx_categories_is_active ON categories(is_active)');
await db.execute('CREATE INDEX idx_printers_code ON printers(code)');
await db.execute('CREATE INDEX idx_printers_type ON printers(type)');
}
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
if (oldVersion < 2) {
// Add printer table in version 2
await db.execute('''
CREATE TABLE printers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
address TEXT,
paper TEXT,
type TEXT,
created_at TEXT,
updated_at TEXT
)
''');
await db.execute('CREATE INDEX idx_printers_code ON printers(code)');
await db.execute('CREATE INDEX idx_printers_type ON printers(type)');
}
if (oldVersion < 3) {
// Add categories table in version 3
await db.execute('''
CREATE TABLE categories (
id TEXT PRIMARY KEY,
organization_id TEXT,
name TEXT NOT NULL,
description TEXT,
business_type TEXT,
metadata TEXT,
is_active INTEGER DEFAULT 1,
created_at TEXT,
updated_at TEXT
)
''');
await db.execute('CREATE INDEX idx_categories_name ON categories(name)');
await db.execute(
'CREATE INDEX idx_categories_organization_id ON categories(organization_id)');
await db.execute(
'CREATE INDEX idx_categories_is_active ON categories(is_active)');
}
}
Future<void> close() async {
final db = await database;
await db.close();
_database = null;
}
}
+29
View File
@@ -0,0 +1,29 @@
import 'package:sqflite/sqflite.dart';
class DatabaseMigrationHandler {
static Future<void> migrate(
Database db, int oldVersion, int newVersion) async {
if (oldVersion < 2) {
// Add indexes for better performance
await db.execute(
'CREATE INDEX IF NOT EXISTS idx_products_name_search ON products(name)');
await db.execute(
'CREATE INDEX IF NOT EXISTS idx_products_sku_search ON products(sku)');
}
if (oldVersion < 3) {
// Add full text search support
await db.execute(
'CREATE VIRTUAL TABLE products_fts USING fts5(name, sku, description, content=products, content_rowid=rowid)');
await db.execute(
'INSERT INTO products_fts SELECT name, sku, description FROM products');
}
if (oldVersion < 4) {
// Add sync tracking
await db.execute('ALTER TABLE products ADD COLUMN last_sync_at TEXT');
await db.execute(
'ALTER TABLE products ADD COLUMN sync_version INTEGER DEFAULT 1');
}
}
}
@@ -0,0 +1,53 @@
import 'dart:developer';
import 'dart:io';
import 'package:sqflite/sqflite.dart';
class DatabaseErrorHandler {
static Future<T> executeWithRetry<T>(
Future<T> Function() operation, {
int maxRetries = 3,
Duration delay = const Duration(milliseconds: 500),
}) async {
int attempts = 0;
while (attempts < maxRetries) {
try {
return await operation();
} catch (e) {
attempts++;
if (attempts >= maxRetries) {
rethrow;
}
log('Database operation failed (attempt $attempts/$maxRetries): $e');
await Future.delayed(delay * attempts);
}
}
throw Exception('Max retries exceeded');
}
static bool isDatabaseCorrupted(dynamic error) {
final errorString = error.toString().toLowerCase();
return errorString.contains('corrupt') ||
errorString.contains('malformed') ||
errorString.contains('no such table');
}
static Future<void> handleDatabaseCorruption() async {
try {
// Delete corrupted database
final dbPath = await getDatabasesPath();
final file = File('$dbPath/pos_database.db');
if (await file.exists()) {
await file.delete();
}
log('Corrupted database deleted, will be recreated');
} catch (e) {
log('Error handling database corruption: $e');
}
}
}
+383 -16
View File
@@ -6,12 +6,13 @@ import 'package:enaklo_pos/core/extensions/string_ext.dart';
import 'package:enaklo_pos/core/utils/printer_service.dart'; import 'package:enaklo_pos/core/utils/printer_service.dart';
import 'package:enaklo_pos/data/dataoutputs/print_dataoutputs.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/auth_local_datasource.dart';
import 'package:enaklo_pos/data/datasources/outlet_local_datasource.dart'; import 'package:enaklo_pos/data/datasources/outlet/outlet_local_datasource.dart';
import 'package:enaklo_pos/data/datasources/product_local_datasource.dart'; import 'package:enaklo_pos/data/datasources/printer/printer_local_datasource.dart';
import 'package:enaklo_pos/data/datasources/settings_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/models/response/order_response_model.dart';
import 'package:enaklo_pos/data/type/bussines_type.dart'; import 'package:enaklo_pos/data/type/bussines_type.dart';
import 'package:enaklo_pos/presentation/home/models/product_quantity.dart'; import 'package:enaklo_pos/presentation/home/models/product_quantity.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:barcode/barcode.dart'; import 'package:barcode/barcode.dart';
import 'package:image/image.dart' as img; import 'package:image/image.dart' as img;
@@ -22,15 +23,63 @@ Future<void> onPrint(
required Order order, required Order order,
}) async { }) async {
final outlet = await OutletLocalDatasource().get(); final outlet = await OutletLocalDatasource().get();
final settings = await SettingsLocalDatasource().getTax();
final authData = await AuthLocalDataSource().getAuthData();
if (outlet.businessType == BusinessType.restaurant) { if (outlet.businessType == BusinessType.restaurant) {
final checkerPrinter = final checkerPrinter =
await ProductLocalDatasource.instance.getPrinterByCode('checker'); await PrinterLocalDatasource.instance.getPrinterByCode('checker');
final kitchenPrinter = final kitchenPrinter =
await ProductLocalDatasource.instance.getPrinterByCode('kitchen'); await PrinterLocalDatasource.instance.getPrinterByCode('kitchen');
final receiptPrinter =
await PrinterLocalDatasource.instance.getPrinterByCode('receipt');
final barPrinter = final barPrinter =
await ProductLocalDatasource.instance.getPrinterByCode('bar'); await PrinterLocalDatasource.instance.getPrinterByCode('bar');
final authData = await AuthLocalDataSource().getAuthData(); if (receiptPrinter != null) {
try {
final printValue = await PrintDataoutputs.instance.printOrderV4(
order,
authData.user?.name ?? "",
'',
0,
0,
settings.value,
receiptPrinter.paper.toIntegerFromText,
order.orderType ?? "",
outlet,
productQuantity);
await PrinterService()
.printWithPrinter(receiptPrinter, printValue, context);
} catch (e, stackTrace) {
FirebaseCrashlytics.instance.recordError(
e,
stackTrace,
reason: 'Print receipt failed',
information: [
'Order ID: ${order.id}',
'Printer: ${receiptPrinter.name}',
],
);
log("Error printing receipt order: $e");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error printing receipt order: $e')),
);
}
} else {
FirebaseCrashlytics.instance.recordError(
'Receipt printer not found',
null,
reason:
'Receipt printer not found / Printer not setting in printer page',
information: [
'Order ID: ${order.id}',
],
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Anda belum menghubungkan printer receipt')),
);
}
// Checker printer // Checker printer
if (checkerPrinter != null) { if (checkerPrinter != null) {
@@ -40,69 +89,151 @@ Future<void> onPrint(
order.tableNumber ?? "", order.tableNumber ?? "",
order.orderNumber ?? "", order.orderNumber ?? "",
authData.user?.name ?? "", authData.user?.name ?? "",
order.metadata?['customer_name'] ?? "",
checkerPrinter.paper.toIntegerFromText, checkerPrinter.paper.toIntegerFromText,
order.orderType ?? "", order.orderType ?? "",
); );
if (productQuantity.isNotEmpty) {
await PrinterService() await PrinterService()
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously
.printWithPrinter(checkerPrinter, printValue, context); .printWithPrinter(checkerPrinter, printValue, context);
} catch (e) { }
} catch (e, stackTrace) {
log("Error printing checker: $e"); log("Error printing checker: $e");
FirebaseCrashlytics.instance.recordError(
e,
stackTrace,
reason: 'Error printing checker ${checkerPrinter.name}',
information: [
'Printer: checker',
'data: ${checkerPrinter.toMap()}',
],
);
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error printing checker: $e')), SnackBar(content: Text('Error printing checker: $e')),
); );
} }
} else {
FirebaseCrashlytics.instance.recordError(
'Checker printer not found',
null,
reason:
'Checker printer not found / Printer not setting in printer page',
information: [
'Order ID: ${order.id}',
],
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Anda belum menghubungkan printer checker')),
);
} }
// Kitchen printer // Kitchen printer
if (kitchenPrinter != null) { if (kitchenPrinter != null) {
try { try {
final printValue = await PrintDataoutputs.instance.printKitchen( final productByPrinter = productQuantity
productQuantity, .where((item) => item.product.printerType == 'kitchen')
order.tableNumber!, .toList();
final printValue =
await PrintDataoutputs.instance.printKitchenPerProduct(
productByPrinter,
order.tableNumber ?? "",
order.orderNumber ?? "", order.orderNumber ?? "",
authData.user?.name ?? "", authData.user?.name ?? "",
order.metadata?['customer_name'] ?? "",
kitchenPrinter.paper.toIntegerFromText, kitchenPrinter.paper.toIntegerFromText,
order.orderType ?? "", order.orderType ?? "",
); );
if (productByPrinter.isNotEmpty) {
await PrinterService() await PrinterService()
.printWithPrinter(kitchenPrinter, printValue, context); .printWithPrinter(kitchenPrinter, printValue, context);
} catch (e) { }
} catch (e, stackTrace) {
log("Error printing kitchen order: $e"); log("Error printing kitchen order: $e");
FirebaseCrashlytics.instance.recordError(
e,
stackTrace,
reason: 'Error printing kitchen ${kitchenPrinter.name}',
information: [
'Printer: kitchen',
'data: ${kitchenPrinter.toMap()}',
],
);
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error printing kitchen order: $e')), SnackBar(content: Text('Error printing kitchen order: $e')),
); );
} }
} else {
FirebaseCrashlytics.instance.recordError(
'Kitchen printer not found',
null,
reason:
'Kitchen printer not found / Printer not setting in printer page',
information: [
'Order ID: ${order.id}',
],
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Anda belum menghubungkan printer kitchen')),
);
} }
// Bar printer // Bar printer
if (barPrinter != null) { if (barPrinter != null) {
try { try {
final productByPrinter = productQuantity
.where((item) => item.product.printerType == 'bar')
.toList();
final printValue = await PrintDataoutputs.instance.printBar( final printValue = await PrintDataoutputs.instance.printBar(
productQuantity, productByPrinter,
order.tableNumber ?? "", order.tableNumber ?? "",
order.orderNumber ?? "", order.orderNumber ?? "",
authData.user?.name ?? "", authData.user?.name ?? "",
order.metadata?['customer_name'] ?? "",
barPrinter.paper.toIntegerFromText, barPrinter.paper.toIntegerFromText,
order.orderType ?? "", order.orderType ?? "",
); );
if (productByPrinter.isNotEmpty) {
await PrinterService() await PrinterService()
.printWithPrinter(barPrinter, printValue, context); .printWithPrinter(barPrinter, printValue, context);
} catch (e) { }
} catch (e, stackTrace) {
log("Error printing bar order: $e"); log("Error printing bar order: $e");
FirebaseCrashlytics.instance.recordError(
e,
stackTrace,
reason: 'Error printing bar ${barPrinter.name}',
information: [
'Printer: bar',
'data: ${barPrinter.toMap()}',
],
);
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error printing bar order: $e')), SnackBar(content: Text('Error printing bar order: $e')),
); );
} }
} else {
FirebaseCrashlytics.instance.recordError(
'Bar printer not found',
null,
reason: 'Bar printer not found / Printer not setting in printer page',
information: [
'Order ID: ${order.id}',
],
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Anda belum menghubungkan printer bar')),
);
} }
} }
if (outlet.businessType == BusinessType.ticketing) { if (outlet.businessType == BusinessType.ticketing) {
final ticketPrinter = final ticketPrinter =
await ProductLocalDatasource.instance.getPrinterByCode('ticket'); await PrinterLocalDatasource.instance.getPrinterByCode('ticket');
final barcode = await generateBarcodeAsUint8List(order.orderNumber ?? ""); final barcode = await generateBarcodeAsUint8List(order.orderNumber ?? "");
@@ -117,12 +248,142 @@ Future<void> onPrint(
await PrinterService() await PrinterService()
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously
.printWithPrinter(ticketPrinter, printValue, context); .printWithPrinter(ticketPrinter, printValue, context);
} catch (e) { } catch (e, stackTrace) {
FirebaseCrashlytics.instance.recordError(
e,
stackTrace,
reason: 'Error printing ticket ${ticketPrinter.name}',
information: [
'Printer: ticket',
'data: ${ticketPrinter.toMap()}',
],
);
log("Error printing ticket: $e"); log("Error printing ticket: $e");
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error printing ticket: $e')), SnackBar(content: Text('Error printing ticket: $e')),
); );
} }
} else {
FirebaseCrashlytics.instance.recordError(
'Ticket printer not found',
null,
reason:
'Ticket printer not found / Printer not setting in printer page',
information: [
'Order ID: ${order.id}',
],
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Anda belum menghubungkan printer ticket')),
);
}
}
}
Future<void> onPrintBill(
BuildContext context, {
required List<ProductQuantity> productQuantity,
required Order order,
}) async {
final outlet = await OutletLocalDatasource().get();
final settings = await SettingsLocalDatasource().getTax();
final authData = await AuthLocalDataSource().getAuthData();
if (outlet.businessType == BusinessType.restaurant) {
final receiptPrinter =
await PrinterLocalDatasource.instance.getPrinterByCode('receipt');
if (receiptPrinter != null) {
try {
final printValue = await PrintDataoutputs.instance.printOrderV4(
order,
authData.user?.name ?? "",
'',
0,
0,
settings.value,
receiptPrinter.paper.toIntegerFromText,
order.orderType ?? "",
outlet,
productQuantity);
await PrinterService()
.printWithPrinter(receiptPrinter, printValue, context);
} catch (e, stackTrace) {
FirebaseCrashlytics.instance.recordError(
e,
stackTrace,
reason: 'Print receipt failed',
information: [
'Order ID: ${order.id}',
'Printer: ${receiptPrinter.name}',
],
);
log("Error printing receipt order: $e");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error printing receipt order: $e')),
);
}
} else {
FirebaseCrashlytics.instance.recordError(
'Receipt printer not found',
null,
reason:
'Receipt printer not found / Printer not setting in printer page',
information: [
'Order ID: ${order.id}',
],
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Anda belum menghubungkan printer receipt')),
);
}
}
if (outlet.businessType == BusinessType.ticketing) {
final ticketPrinter =
await PrinterLocalDatasource.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, stackTrace) {
FirebaseCrashlytics.instance.recordError(
e,
stackTrace,
reason: 'Error printing ticket ${ticketPrinter.name}',
information: [
'Printer: ticket',
'data: ${ticketPrinter.toMap()}',
],
);
log("Error printing ticket: $e");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error printing ticket: $e')),
);
}
} else {
FirebaseCrashlytics.instance.recordError(
'Ticket printer not found',
null,
reason:
'Ticket printer not found / Printer not setting in printer page',
information: [
'Order ID: ${order.id}',
],
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Anda belum menghubungkan printer ticket')),
);
} }
} }
} }
@@ -133,9 +394,10 @@ Future<void> onPrintRecipt(
required String paymentMethod, required String paymentMethod,
required int nominalBayar, required int nominalBayar,
required int kembalian, required int kembalian,
required List<ProductQuantity> productQuantity,
}) async { }) async {
final receiptPrinter = final receiptPrinter =
await ProductLocalDatasource.instance.getPrinterByCode('receipt'); await PrinterLocalDatasource.instance.getPrinterByCode('receipt');
final authData = await AuthLocalDataSource().getAuthData(); final authData = await AuthLocalDataSource().getAuthData();
final settings = await SettingsLocalDatasource().getTax(); final settings = await SettingsLocalDatasource().getTax();
final outlet = await OutletLocalDatasource().get(); final outlet = await OutletLocalDatasource().get();
@@ -152,7 +414,62 @@ Future<void> onPrintRecipt(
receiptPrinter.paper.toIntegerFromText, receiptPrinter.paper.toIntegerFromText,
order.orderType ?? "", order.orderType ?? "",
outlet, outlet,
productQuantity);
await PrinterService()
.printWithPrinter(receiptPrinter, printValue, context);
} catch (e, stackTrace) {
FirebaseCrashlytics.instance.recordError(
e,
stackTrace,
reason: 'Print receipt failed',
information: [
'Order ID: ${order.id}',
'Payment Method: $paymentMethod',
'Printer: ${receiptPrinter.name}',
],
); );
log("Error printing receipt order: $e");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error printing receipt order: $e')),
);
}
} else {
FirebaseCrashlytics.instance.recordError(
'Kitchen printer not found',
null,
reason: 'Kitchen printer not found / Printer not setting in printer page',
information: [
'Order ID: ${order.id}',
],
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Anda belum menghubungkan printer kitchen')),
);
}
}
Future<void> onPrinVoidRecipt(
context, {
required Order order,
required List<OrderItem> productItemVoid,
required int totalVoid,
}) async {
final receiptPrinter =
await PrinterLocalDatasource.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.printVoidOrder(
order,
authData.user?.name ?? "",
settings.value,
receiptPrinter.paper.toIntegerFromText,
totalVoid,
outlet,
productItemVoid);
await PrinterService() await PrinterService()
.printWithPrinter(receiptPrinter, printValue, context); .printWithPrinter(receiptPrinter, printValue, context);
} catch (e) { } catch (e) {
@@ -164,6 +481,56 @@ Future<void> onPrintRecipt(
} }
} }
Future<void> onPrintSplit(
context, {
required Order order,
}) async {
final receiptPrinter =
await PrinterLocalDatasource.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.printSplitBill(
order,
authData.user?.name ?? "",
receiptPrinter.paper.toIntegerFromText,
outlet,
);
await PrinterService()
.printWithPrinter(receiptPrinter, printValue, context);
} catch (e, stackTrace) {
FirebaseCrashlytics.instance.recordError(
e,
stackTrace,
reason: 'Print receipt failed',
information: [
'Order ID: ${order.id}',
'Printer: ${receiptPrinter.name}',
],
);
log("Error printing receipt order: $e");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error printing receipt order: $e')),
);
}
} else {
FirebaseCrashlytics.instance.recordError(
'Kitchen printer not found',
null,
reason: 'Kitchen printer not found / Printer not setting in printer page',
information: [
'Order ID: ${order.id}',
],
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Anda belum menghubungkan printer kitchen')),
);
}
}
Future<Uint8List> generateBarcodeAsUint8List(String data) async { Future<Uint8List> generateBarcodeAsUint8List(String data) async {
// 1. Buat barcode instance (code128, qrCode, dll) // 1. Buat barcode instance (code128, qrCode, dll)
final barcode = Barcode.code128(); final barcode = Barcode.code128();
+18 -6
View File
@@ -1,14 +1,17 @@
import 'package:awesome_dio_interceptor/awesome_dio_interceptor.dart'; import 'package:awesome_dio_interceptor/awesome_dio_interceptor.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:enaklo_pos/core/constants/variables.dart';
import 'package:enaklo_pos/core/extensions/build_context_ext.dart'; import 'package:enaklo_pos/core/extensions/build_context_ext.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/presentation/auth/login_page.dart'; import 'package:enaklo_pos/presentation/auth/login_page.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
class DioClient { class DioClient {
static final Dio _dio = Dio(BaseOptions( static final Dio _dio = Dio(BaseOptions(
connectTimeout: const Duration(seconds: 10), connectTimeout: const Duration(minutes: 1),
receiveTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(minutes: 1),
headers: { headers: {
'Accept': 'application/json', 'Accept': 'application/json',
}, },
@@ -124,13 +127,17 @@ class AuthInterceptorWithRefresh extends Interceptor {
Future<bool> _tryRefreshToken() async { Future<bool> _tryRefreshToken() async {
try { try {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
final refreshToken = prefs.getString('refresh_token'); final authData = await AuthLocalDataSource().getAuthData();
if (refreshToken == null) return false; final url = '${Variables.baseUrl}/api/v1/auth/refresh';
final response = await Dio().post( final response = await Dio().post(
'YOUR_REFRESH_TOKEN_ENDPOINT', url,
data: {'refresh_token': refreshToken}, options: Options(
headers: {
'Authorization': 'Bearer ${authData.refreshToken}',
},
),
); );
if (response.statusCode == 200) { if (response.statusCode == 200) {
@@ -140,6 +147,11 @@ class AuthInterceptorWithRefresh extends Interceptor {
await prefs.setString('auth_token', newToken); await prefs.setString('auth_token', newToken);
await prefs.setString('refresh_token', newRefreshToken); await prefs.setString('refresh_token', newRefreshToken);
AuthResponseModel authResponseModel =
AuthResponseModel.fromMap(response.data['data']);
AuthLocalDataSource().saveAuthData(authResponseModel);
return true; return true;
} }
} catch (e) { } catch (e) {
@@ -0,0 +1,64 @@
import 'dart:developer';
class DatabasePerformanceMonitor {
static final Map<String, List<int>> _queryTimes = {};
static Future<T> monitorQuery<T>(
String queryName,
Future<T> Function() query,
) async {
final stopwatch = Stopwatch()..start();
try {
final result = await query();
stopwatch.stop();
_recordQueryTime(queryName, stopwatch.elapsedMilliseconds);
return result;
} catch (e) {
stopwatch.stop();
log('Query "$queryName" failed after ${stopwatch.elapsedMilliseconds}ms: $e');
rethrow;
}
}
static void _recordQueryTime(String queryName, int milliseconds) {
if (!_queryTimes.containsKey(queryName)) {
_queryTimes[queryName] = [];
}
_queryTimes[queryName]!.add(milliseconds);
// Keep only last 100 entries
if (_queryTimes[queryName]!.length > 100) {
_queryTimes[queryName]!.removeAt(0);
}
// Log slow queries
if (milliseconds > 1000) {
log('Slow query detected: "$queryName" took ${milliseconds}ms');
}
}
static Map<String, dynamic> getPerformanceStats() {
final stats = <String, dynamic>{};
_queryTimes.forEach((queryName, times) {
if (times.isNotEmpty) {
final avgTime = times.reduce((a, b) => a + b) / times.length;
final maxTime = times.reduce((a, b) => a > b ? a : b);
final minTime = times.reduce((a, b) => a < b ? a : b);
stats[queryName] = {
'average_ms': avgTime.round(),
'max_ms': maxTime,
'min_ms': minTime,
'total_queries': times.length,
};
}
});
return stats;
}
}
+568
View File
@@ -0,0 +1,568 @@
import 'dart:io';
import 'package:enaklo_pos/core/utils/helper_pdf_service.dart';
import 'package:enaklo_pos/data/datasources/outlet/outlet_local_datasource.dart';
import 'package:enaklo_pos/data/models/response/inventory_analytic_response_model.dart';
import 'package:flutter/services.dart';
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
class InventoryReport {
static final primaryColor = PdfColor.fromHex("36175e");
static Future<File> previewPdf({
required String searchDateFormatted,
required InventoryAnalyticData? inventory,
}) async {
final pdf = pw.Document();
final ByteData dataImage = await rootBundle.load('assets/logo/logo.png');
final Uint8List bytes = dataImage.buffer.asUint8List();
final outlet = await OutletLocalDatasource().get();
final image = pw.MemoryImage(bytes);
pdf.addPage(
pw.MultiPage(
pageFormat: PdfPageFormat.a4,
margin: pw.EdgeInsets.zero,
build: (pw.Context context) {
return [
pw.Container(
padding: pw.EdgeInsets.all(20),
child: pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
// Bagian kiri - Logo dan Info Perusahaan
pw.Row(
crossAxisAlignment: pw.CrossAxisAlignment.center,
children: [
// Icon/Logo placeholder (bisa diganti dengan gambar logo)
pw.Container(
width: 40,
height: 40,
child: pw.Image(image),
),
pw.SizedBox(width: 15),
pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Text(
'Apskel',
style: pw.TextStyle(
fontSize: 28,
fontWeight: pw.FontWeight.bold,
color: primaryColor,
),
),
pw.SizedBox(height: 4),
pw.Text(
outlet.name ?? "",
style: pw.TextStyle(
fontSize: 16,
color: PdfColors.grey700,
),
),
pw.SizedBox(height: 2),
pw.Text(
outlet.address ?? "",
style: pw.TextStyle(
fontSize: 12,
color: PdfColors.grey600,
),
),
],
),
],
),
// Bagian kanan - Info Laporan
pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.end,
children: [
pw.Text(
'Laporan Transaksi',
style: pw.TextStyle(
fontSize: 24,
fontWeight: pw.FontWeight.bold,
color: PdfColors.grey800,
),
),
pw.SizedBox(height: 8),
pw.Text(
searchDateFormatted,
style: pw.TextStyle(
fontSize: 14,
color: PdfColors.grey600,
),
),
pw.SizedBox(height: 4),
pw.Text(
'Laporan',
style: pw.TextStyle(
fontSize: 12,
color: PdfColors.grey500,
),
),
],
),
],
),
),
pw.Container(
width: double.infinity,
height: 3,
color: primaryColor,
),
// Summary
pw.Container(
padding: pw.EdgeInsets.all(20),
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
_buildSectionWidget('1. Ringkasan'),
pw.SizedBox(height: 30),
pw.Row(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Expanded(
flex: 1,
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
_buildSummaryItem(
'Total Item',
(inventory?.summary.totalProducts ?? 0)
.toString(),
),
_buildSummaryItem(
'Total Item Masuk',
(inventory?.products.fold<num>(
0,
(sum, item) =>
sum + (item.totalIn)) ??
0)
.toString(),
),
_buildSummaryItem(
'Total Item Keluar',
(inventory?.products.fold<num>(0,
(sum, item) => sum + (item.totalOut)))
.toString(),
),
],
),
),
pw.SizedBox(width: 20),
pw.Expanded(
flex: 1,
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
_buildSummaryItem(
'Total Ingredient',
(inventory?.summary.totalIngredients ?? 0)
.toString(),
),
_buildSummaryItem(
'Total Ingredient Masuk',
(inventory?.ingredients.fold<num>(
0,
(sum, item) =>
sum + (item.totalIn)) ??
0)
.toString(),
),
_buildSummaryItem(
'Total Ingredient Keluar',
(inventory?.ingredients.fold<num>(
0,
(sum, item) =>
sum + (item.totalOut)) ??
0)
.toString(),
),
],
),
),
],
),
],
),
),
// Summary Item
pw.Container(
padding: pw.EdgeInsets.all(20),
child: pw.Column(
children: [
pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
_buildSectionWidget('2. Item'),
pw.SizedBox(height: 30),
pw.Container(
decoration: pw.BoxDecoration(
color: primaryColor, // Purple color
borderRadius: pw.BorderRadius.only(
topLeft: pw.Radius.circular(8),
topRight: pw.Radius.circular(8),
),
),
child: pw.Table(
columnWidths: const {
0: pw.FlexColumnWidth(2.5), // Produk
1: pw.FlexColumnWidth(2), // Kategori
2: pw.FlexColumnWidth(1), // Stock
3: pw.FlexColumnWidth(2), // Masuk
4: pw.FlexColumnWidth(2), // Keluar
},
children: [
pw.TableRow(
children: [
_buildHeaderCell('Nama'),
_buildHeaderCell('Kategori'),
_buildHeaderCell('Stock'),
_buildHeaderCell('Masuk'),
_buildHeaderCell('Keluar'),
],
),
],
),
),
pw.Container(
decoration: pw.BoxDecoration(
color: PdfColors.white,
),
child: pw.Table(
columnWidths: {
0: pw.FlexColumnWidth(2.5), // Produk
1: pw.FlexColumnWidth(2), // Kategori
2: pw.FlexColumnWidth(1), // Stock
3: pw.FlexColumnWidth(2), // Masuk
4: pw.FlexColumnWidth(2), // Keluar
},
children: inventory?.products
.map((item) => _buildProductDataRow(
item,
inventory.products.indexOf(item) %
2 ==
0,
))
.toList() ??
[],
),
),
pw.Container(
decoration: pw.BoxDecoration(
color: primaryColor, // Purple color
borderRadius: pw.BorderRadius.only(
bottomLeft: pw.Radius.circular(8),
bottomRight: pw.Radius.circular(8),
),
),
child: pw.Table(
columnWidths: const {
0: pw.FlexColumnWidth(2.5), // Produk
1: pw.FlexColumnWidth(2), // Kategori
2: pw.FlexColumnWidth(1), // Stock
3: pw.FlexColumnWidth(2), // Masuk
4: pw.FlexColumnWidth(2), // Keluar
},
children: [
pw.TableRow(
children: [
_buildTotalCell('TOTAL'),
_buildTotalCell(''),
_buildTotalCell(
(inventory?.products.fold<num>(
0,
(sum, item) =>
sum + (item.quantity)) ??
0)
.toString(),
),
_buildTotalCell(
(inventory?.products.fold<num>(
0,
(sum, item) =>
sum + (item.totalIn)) ??
0)
.toString(),
),
_buildTotalCell(
(inventory?.products.fold<num>(
0,
(sum, item) =>
sum + (item.totalOut)) ??
0)
.toString(),
),
],
),
],
),
),
],
),
],
),
),
// Summary Ingredient
pw.Container(
padding: pw.EdgeInsets.all(20),
child: pw.Column(
children: [
pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
_buildSectionWidget('3. Ingredient'),
pw.SizedBox(height: 30),
pw.Container(
decoration: pw.BoxDecoration(
color: primaryColor, // Purple color
borderRadius: pw.BorderRadius.only(
topLeft: pw.Radius.circular(8),
topRight: pw.Radius.circular(8),
),
),
child: pw.Table(
columnWidths: const {
0: pw.FlexColumnWidth(2.5), // Name
1: pw.FlexColumnWidth(1), // Stock
2: pw.FlexColumnWidth(2), // Masuk
3: pw.FlexColumnWidth(2), // Keluar
},
children: [
pw.TableRow(
children: [
_buildHeaderCell('Nama'),
_buildHeaderCell('Stock'),
_buildHeaderCell('Masuk'),
_buildHeaderCell('Keluar'),
],
),
],
),
),
pw.Container(
decoration: pw.BoxDecoration(
color: PdfColors.white,
),
child: pw.Table(
columnWidths: {
0: pw.FlexColumnWidth(2.5), // Name
1: pw.FlexColumnWidth(1), // Stock
2: pw.FlexColumnWidth(2), // Masuk
3: pw.FlexColumnWidth(2), // Keluar
},
children: inventory?.ingredients
.map((item) => _buildIngredientsDataRow(
item,
inventory.ingredients.indexOf(item) %
2 ==
0,
))
.toList() ??
[],
),
),
pw.Container(
decoration: pw.BoxDecoration(
color: primaryColor, // Purple color
borderRadius: pw.BorderRadius.only(
bottomLeft: pw.Radius.circular(8),
bottomRight: pw.Radius.circular(8),
),
),
child: pw.Table(
columnWidths: const {
0: pw.FlexColumnWidth(2.5), // Name
1: pw.FlexColumnWidth(1), // Stock
2: pw.FlexColumnWidth(2), // Masuk
3: pw.FlexColumnWidth(2), // Keluar
},
children: [
pw.TableRow(
children: [
_buildTotalCell('TOTAL'),
_buildTotalCell(
(inventory?.ingredients.fold<num>(
0,
(sum, item) =>
sum + (item.quantity)) ??
0)
.toString(),
),
_buildTotalCell(
(inventory?.ingredients.fold<num>(
0,
(sum, item) =>
sum + (item.totalIn)) ??
0)
.toString(),
),
_buildTotalCell(
(inventory?.ingredients.fold<num>(
0,
(sum, item) =>
sum + (item.totalOut)) ??
0)
.toString(),
),
],
),
],
),
),
],
),
],
),
),
];
}),
);
return HelperPdfService.saveDocument(
name:
'Apskel POS | Inventory Report | ${DateTime.now().millisecondsSinceEpoch}.pdf',
pdf: pdf);
}
static pw.Widget _buildSectionWidget(String title) {
return pw.Text(
title,
style: pw.TextStyle(
fontSize: 20,
fontWeight: pw.FontWeight.bold,
color: primaryColor,
),
);
}
static pw.Widget _buildSummaryItem(
String label,
String value, {
pw.TextStyle? valueStyle,
pw.TextStyle? labelStyle,
}) {
return pw.Container(
padding: pw.EdgeInsets.only(bottom: 8),
margin: pw.EdgeInsets.only(bottom: 16),
decoration: pw.BoxDecoration(
border: pw.Border(
bottom: pw.BorderSide(
color: PdfColors.grey300,
),
),
),
child: pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
children: [
pw.Text(label, style: labelStyle),
pw.Text(
value,
style: valueStyle ??
pw.TextStyle(
fontWeight: pw.FontWeight.bold,
),
),
],
),
);
}
static pw.Widget _buildHeaderCell(String text) {
return pw.Container(
padding: pw.EdgeInsets.symmetric(horizontal: 12, vertical: 16),
child: pw.Text(
text,
style: pw.TextStyle(
color: PdfColors.white,
fontWeight: pw.FontWeight.bold,
fontSize: 12,
),
textAlign: pw.TextAlign.center,
),
);
}
static pw.Widget _buildDataCell(String text,
{pw.Alignment alignment = pw.Alignment.center, PdfColor? textColor}) {
return pw.Container(
padding: pw.EdgeInsets.symmetric(horizontal: 12, vertical: 16),
alignment: alignment,
child: pw.Text(
text,
style: pw.TextStyle(
fontSize: 12,
color: textColor ?? PdfColors.black,
fontWeight: pw.FontWeight.normal,
),
textAlign: alignment == pw.Alignment.centerLeft
? pw.TextAlign.left
: pw.TextAlign.center,
),
);
}
static pw.Widget _buildTotalCell(String text) {
return pw.Container(
padding: pw.EdgeInsets.symmetric(horizontal: 12, vertical: 16),
child: pw.Text(
text,
style: pw.TextStyle(
color: PdfColors.white,
fontWeight: pw.FontWeight.bold,
fontSize: 12,
),
textAlign: pw.TextAlign.center,
),
);
}
static pw.TableRow _buildProductDataRow(
InventoryProductItem product, bool isEven) {
return pw.TableRow(
decoration: pw.BoxDecoration(
color: product.isZeroStock
? PdfColors.red100
: product.isLowStock
? PdfColors.yellow100
: isEven
? PdfColors.grey50
: PdfColors.white,
),
children: [
_buildDataCell(product.productName, alignment: pw.Alignment.centerLeft),
_buildDataCell(product.categoryName,
alignment: pw.Alignment.centerLeft),
_buildDataCell(product.quantity.toString()),
_buildDataCell(product.totalIn.toString()),
_buildDataCell(product.totalOut.toString()),
],
);
}
static pw.TableRow _buildIngredientsDataRow(
InventoryIngredientItem item, bool isEven) {
return pw.TableRow(
decoration: pw.BoxDecoration(
color: item.isZeroStock
? PdfColors.red100
: item.isLowStock
? PdfColors.yellow100
: isEven
? PdfColors.grey50
: PdfColors.white,
),
children: [
_buildDataCell(item.ingredientName, alignment: pw.Alignment.centerLeft),
_buildDataCell(item.quantity.toString()),
_buildDataCell(item.totalIn.toString()),
_buildDataCell(item.totalOut.toString()),
],
);
}
}
+157 -48
View File
@@ -1,14 +1,19 @@
import 'dart:developer'; import 'dart:developer';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_esc_pos_network/flutter_esc_pos_network.dart'; import 'package:flutter_esc_pos_network/flutter_esc_pos_network.dart';
import 'package:print_bluetooth_thermal/print_bluetooth_thermal.dart'; import 'package:print_bluetooth_thermal/print_bluetooth_thermal.dart';
import 'package:enaklo_pos/data/models/response/print_model.dart'; import 'package:enaklo_pos/data/models/response/print_model.dart';
import 'package:synchronized/synchronized.dart';
class PrinterService { class PrinterService {
static final PrinterService _instance = PrinterService._internal(); static final PrinterService _instance = PrinterService._internal();
factory PrinterService() => _instance; factory PrinterService() => _instance;
PrinterService._internal(); PrinterService._internal();
final Map<String, Lock> _locks = {};
String? _connectedMac;
/// Connect to Bluetooth printer /// Connect to Bluetooth printer
Future<bool> connectBluetoothPrinter(String macAddress) async { Future<bool> connectBluetoothPrinter(String macAddress) async {
try { try {
@@ -20,22 +25,106 @@ class PrinterService {
} }
// Connect to the printer // Connect to the printer
bool connected = await PrintBluetoothThermal.connect( bool connected =
macPrinterAddress: macAddress); await PrintBluetoothThermal.connect(macPrinterAddress: macAddress);
if (connected) { if (connected) {
log("Successfully connected to Bluetooth printer: $macAddress"); log("Successfully connected to Bluetooth printer: $macAddress");
} else { } else {
FirebaseCrashlytics.instance.recordError(
'Failed to connect to Bluetooth printer',
null,
reason: 'Failed to connect to Bluetooth printe',
information: [
'function: connectBluetoothPrinter(String macAddress)',
'macAddress: $macAddress',
],
);
log("Failed to connect to Bluetooth printer: $macAddress"); log("Failed to connect to Bluetooth printer: $macAddress");
} }
return connected; return connected;
} catch (e) { } catch (e, stackTrace) {
FirebaseCrashlytics.instance.recordError(
e,
stackTrace,
reason: 'Error connecting to Bluetooth printer',
information: [
'function: connectBluetoothPrinter(String macAddress)',
'Printer: Bluetooth printer',
'macAddress: $macAddress',
],
);
log("Error connecting to Bluetooth printer: $e"); log("Error connecting to Bluetooth printer: $e");
return false; return false;
} }
} }
Lock _getLock(String address) {
return _locks.putIfAbsent(address, () => Lock());
}
Future<bool> printWithPrinter(
PrintModel printer, List<int> printData, BuildContext context) async {
if (printer.type == 'Bluetooth') {
return await _getLock(printer.address).synchronized(() async {
try {
bool isConnected = await PrintBluetoothThermal.connectionStatus;
if (!isConnected || _connectedMac != printer.address) {
if (isConnected) {
await PrintBluetoothThermal.disconnect;
await Future.delayed(const Duration(milliseconds: 1500));
}
bool connected = await PrintBluetoothThermal.connect(
macPrinterAddress: printer.address);
if (!connected) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Gagal connect ke ${printer.name}')),
);
}
return false;
}
_connectedMac = printer.address;
await Future.delayed(
const Duration(milliseconds: 1000)); // naikkan dari 500ms
}
bool result = await _writeBytesChunked(printData);
// Beri waktu printer flush sebelum job berikutnya
await Future.delayed(const Duration(milliseconds: 1000));
log("Print result ${printer.name}: $result");
return result;
} catch (e, stackTrace) {
_connectedMac = null;
FirebaseCrashlytics.instance.recordError(e, stackTrace,
reason: 'Error printing ${printer.name}',
information: [
'Printer: ${printer.name}',
'MAC: ${printer.address}'
]);
return false;
}
});
} else {
return await printNetwork(printer, printData, context);
}
}
// Kirim data per chunk untuk hindari buffer overflow Bluetooth
Future<bool> _writeBytesChunked(List<int> data, {int chunkSize = 512}) async {
for (int i = 0; i < data.length; i += chunkSize) {
final end = (i + chunkSize > data.length) ? data.length : i + chunkSize;
final chunk = data.sublist(i, end);
bool result = await PrintBluetoothThermal.writeBytes(chunk);
if (!result) return false;
await Future.delayed(const Duration(milliseconds: 30));
}
return true;
}
/// Print using Bluetooth printer /// Print using Bluetooth printer
Future<bool> printBluetooth(List<int> printData) async { Future<bool> printBluetooth(List<int> printData) async {
try { try {
@@ -49,80 +138,100 @@ class PrinterService {
if (printResult) { if (printResult) {
log("Successfully printed via Bluetooth"); log("Successfully printed via Bluetooth");
} else { } else {
FirebaseCrashlytics.instance.recordError(
'Failed to print via Bluetooth',
null,
reason: 'Failed to print via Bluetooth',
information: [
'function: printBluetooth(List<int> printData)',
'printData: $printData',
],
);
log("Failed to print via Bluetooth"); log("Failed to print via Bluetooth");
} }
return printResult; return printResult;
} catch (e) { } catch (e, stackTrace) {
FirebaseCrashlytics.instance.recordError(
e,
stackTrace,
reason: 'Error printing via Bluetooth',
information: [
'function: printBluetooth(List<int> printData)',
'Printer: Bluetooth printer',
'printData: $printData',
],
);
log("Error printing via Bluetooth: $e"); log("Error printing via Bluetooth: $e");
return false; return false;
} }
} }
/// Print using Network printer /// Print using Network printer
Future<bool> printNetwork(String ipAddress, List<int> printData) async { Future<bool> printNetwork(
PrintModel printer, List<int> printData, BuildContext context) async {
try { try {
final printer = PrinterNetworkManager(ipAddress); final networkPrinter = PrinterNetworkManager(printer.address);
PosPrintResult connect = await printer.connect(); PosPrintResult connect = await networkPrinter.connect();
if (connect == PosPrintResult.success) { if (connect == PosPrintResult.success) {
PosPrintResult printing = await printer.printTicket(printData); PosPrintResult printing = await networkPrinter.printTicket(printData);
printer.disconnect(); networkPrinter.disconnect();
if (printing == PosPrintResult.success) { if (printing == PosPrintResult.success) {
log("Successfully printed via Network printer: $ipAddress"); log("Successfully printed via Network printer: ${printer.address}");
return true; return true;
} else { } else {
FirebaseCrashlytics.instance.recordError(
'Failed to print via Network printer: ${printing.msg}',
null,
reason: 'Failed to print via Network printer',
information: [
'Printer: ${printer.name}',
'ipAddress: ${printer.address}',
],
);
log("Failed to print via Network printer: ${printing.msg}"); log("Failed to print via Network printer: ${printing.msg}");
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Gagal print ke ${printer.name}')),
);
}
return false; return false;
} }
} else { } else {
FirebaseCrashlytics.instance.recordError(
'Failed to connect to Network printer: ${connect.msg}',
null,
reason: 'Failed to connect Network printer',
information: [
'Printer: ${printer.name}',
'ipAddress: ${printer.address}',
],
);
log("Failed to connect to Network printer: ${connect.msg}"); log("Failed to connect to Network printer: ${connect.msg}");
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Gagal connect ke ${printer.name}')),
);
}
return false; return false;
} }
} catch (e) { } catch (e, stackTrace) {
FirebaseCrashlytics.instance.recordError(
e,
stackTrace,
reason: 'Error printing via Network',
information: [
'Printer: ${printer.name}',
'ipAddress: ${printer.address}',
],
);
log("Error printing via Network: $e"); log("Error printing via Network: $e");
return false; return false;
} }
} }
/// Print with automatic printer type detection
Future<bool> printWithPrinter(PrintModel printer, List<int> printData, BuildContext context) async {
try {
if (printer.type == 'Bluetooth') {
bool connected = await connectBluetoothPrinter(printer.address);
if (!connected) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to connect to ${printer.name}')),
);
return false;
}
bool printResult = await printBluetooth(printData);
if (!printResult) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to print to ${printer.name}')),
);
}
return printResult;
} else {
bool printResult = await printNetwork(printer.address, printData);
if (!printResult) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to print to ${printer.name}')),
);
}
return printResult;
}
} catch (e) {
log("Error printing with printer ${printer.name}: $e");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error printing to ${printer.name}: $e')),
);
return false;
}
}
/// Disconnect from Bluetooth printer /// Disconnect from Bluetooth printer
Future<bool> disconnectBluetooth() async { Future<bool> disconnectBluetooth() async {
try { try {
+912
View File
@@ -0,0 +1,912 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:enaklo_pos/core/extensions/string_ext.dart';
import 'package:enaklo_pos/core/utils/helper_pdf_service.dart';
import 'package:enaklo_pos/data/models/response/category_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/presentation/home/models/outlet_model.dart';
import 'package:flutter/services.dart';
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
class TransactionReport {
static final primaryColor = PdfColor.fromHex("36175e");
static Future<File> previewPdf({
required Outlet outlet,
required String searchDateFormatted,
required CategoryAnalyticData? categoryAnalyticData,
required ProfitLossData? profitLossData,
required PaymentMethodAnalyticData? paymentMethodAnalyticData,
required ProductAnalyticData? productAnalyticData,
}) async {
final pdf = pw.Document();
final ByteData dataImage = await rootBundle.load('assets/logo/logo.png');
final Uint8List bytes = dataImage.buffer.asUint8List();
final profitLossProductSummary = {
'totalRevenue': profitLossData?.productData
.fold<num>(0, (sum, item) => sum + (item.revenue)) ??
0,
'totalCost': profitLossData?.productData
.fold<num>(0, (sum, item) => sum + (item.cost)) ??
0,
'totalGrossProfit': profitLossData?.productData
.fold<num>(0, (sum, item) => sum + (item.grossProfit)) ??
0,
'totalQuantity': profitLossData?.productData
.fold<num>(0, (sum, item) => sum + (item.quantitySold)) ??
0,
};
final categorySummary = {
'totalRevenue': categoryAnalyticData?.data
.fold<num>(0, (sum, item) => sum + (item.totalRevenue)) ??
0,
'orderCount': categoryAnalyticData?.data
.fold<num>(0, (sum, item) => sum + (item.orderCount)) ??
0,
'productCount': categoryAnalyticData?.data
.fold<num>(0, (sum, item) => sum + (item.productCount)) ??
0,
'totalQuantity': categoryAnalyticData?.data
.fold<num>(0, (sum, item) => sum + (item.totalQuantity)) ??
0,
};
final productItemSummary = {
'totalRevenue': productAnalyticData?.data
.fold<num>(0, (sum, item) => sum + (item.revenue)) ??
0,
'orderCount': productAnalyticData?.data
.fold<num>(0, (sum, item) => sum + (item.orderCount)) ??
0,
'totalQuantitySold': productAnalyticData?.data
.fold<num>(0, (sum, item) => sum + (item.quantitySold)) ??
0,
};
// Membuat objek Image dari gambar
final image = pw.MemoryImage(bytes);
pdf.addPage(
pw.MultiPage(
pageFormat: PdfPageFormat.a4,
margin: pw.EdgeInsets.zero,
build: (pw.Context context) {
return [
pw.Container(
padding: pw.EdgeInsets.all(20),
child: pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
// Bagian kiri - Logo dan Info Perusahaan
pw.Row(
crossAxisAlignment: pw.CrossAxisAlignment.center,
children: [
// Icon/Logo placeholder (bisa diganti dengan gambar logo)
pw.Container(
width: 40,
height: 40,
child: pw.Image(image),
),
pw.SizedBox(width: 15),
pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Text(
'Apskel',
style: pw.TextStyle(
fontSize: 28,
fontWeight: pw.FontWeight.bold,
color: primaryColor,
),
),
pw.SizedBox(height: 4),
pw.Text(
outlet.name ?? "",
style: pw.TextStyle(
fontSize: 16,
color: PdfColors.grey700,
),
),
pw.SizedBox(height: 2),
pw.Text(
outlet.address ?? "",
style: pw.TextStyle(
fontSize: 12,
color: PdfColors.grey600,
),
),
],
),
],
),
// Bagian kanan - Info Laporan
pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.end,
children: [
pw.Text(
'Laporan Transaksi',
style: pw.TextStyle(
fontSize: 24,
fontWeight: pw.FontWeight.bold,
color: PdfColors.grey800,
),
),
pw.SizedBox(height: 8),
pw.Text(
searchDateFormatted,
style: pw.TextStyle(
fontSize: 14,
color: PdfColors.grey600,
),
),
pw.SizedBox(height: 4),
pw.Text(
'Laporan',
style: pw.TextStyle(
fontSize: 12,
color: PdfColors.grey500,
),
),
],
),
],
),
),
pw.Container(
width: double.infinity,
height: 3,
color: primaryColor,
),
// Summary
pw.Container(
padding: pw.EdgeInsets.all(20),
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
_buildSectionWidget('1. Ringkasan'),
pw.SizedBox(height: 30),
pw.Row(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Expanded(
flex: 1,
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
_buildSummaryItem(
'Total Penjualan (termasuk rasik)',
(profitLossData?.summary.totalRevenue ?? 0)
.toString()
.currencyFormatRpV2,
),
_buildSummaryItem(
'Total Terjual',
(profitLossData?.summary.totalOrders ?? 0)
.toString(),
),
_buildSummaryItem(
'HPP',
'${(profitLossData?.summary.totalCost ?? 0).toString().currencyFormatRpV2} | ${(((profitLossData?.summary.totalCost ?? 0) / (profitLossData?.summary.totalRevenue ?? 1)) * 100).round()}%',
),
_buildSummaryItem(
'Laba Kotor',
'${(profitLossData?.summary.grossProfit ?? 0).toString().currencyFormatRpV2} | ${(profitLossData?.summary.grossProfitMargin ?? 0).round()}%',
valueStyle: pw.TextStyle(
color: PdfColors.green800,
fontWeight: pw.FontWeight.bold,
fontSize: 16,
),
labelStyle: pw.TextStyle(
color: PdfColors.green800,
fontWeight: pw.FontWeight.bold,
fontSize: 16,
),
),
],
),
),
pw.SizedBox(width: 20),
pw.Expanded(
flex: 1,
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
_buildSummaryItem(
'Biaya Lain lain',
'${(profitLossData?.summary.totalTax ?? 0).toString().currencyFormatRpV2} | ${(((profitLossData?.summary.totalTax ?? 0) / (profitLossData?.summary.totalRevenue ?? 1)) * 100).round()}%',
),
_buildSummaryItem(
'Laba/Rugi',
'${(profitLossData?.summary.netProfit ?? 0).toString().currencyFormatRpV2} | ${(profitLossData?.summary.netProfitMargin ?? 0).round()}%',
valueStyle: pw.TextStyle(
color: PdfColors.blue800,
fontWeight: pw.FontWeight.bold,
fontSize: 16,
),
labelStyle: pw.TextStyle(
color: PdfColors.blue800,
fontWeight: pw.FontWeight.bold,
fontSize: 16,
),
),
],
),
),
],
),
pw.SizedBox(height: 16),
pw.Text(
"Laba Rugi Perproduk",
style: pw.TextStyle(
fontSize: 16,
fontWeight: pw.FontWeight.bold,
color: primaryColor,
),
),
pw.SizedBox(height: 20),
pw.Column(
children: [
pw.Container(
decoration: pw.BoxDecoration(
color: primaryColor, // Purple color
borderRadius: pw.BorderRadius.only(
topLeft: pw.Radius.circular(8),
topRight: pw.Radius.circular(8),
),
),
child: pw.Table(
columnWidths: const {
0: pw.FlexColumnWidth(2.5), // Produk
1: pw.FlexColumnWidth(1), // Qty
2: pw.FlexColumnWidth(2.5), // Pendapatan
3: pw.FlexColumnWidth(2), // HPP
4: pw.FlexColumnWidth(2), // Laba Kotor
5: pw.FlexColumnWidth(2), // Margin (%)
},
children: [
pw.TableRow(
children: [
_buildHeaderCell('Produk'),
_buildHeaderCell('Qty'),
_buildHeaderCell('Pendapatan'),
_buildHeaderCell('HPP'),
_buildHeaderCell('Laba Kotor'),
_buildHeaderCell('Margin (%)'),
],
),
],
),
),
pw.Container(
decoration: pw.BoxDecoration(
color: PdfColors.white,
),
child: pw.Table(
columnWidths: {
0: pw.FlexColumnWidth(2.5), // Produk
1: pw.FlexColumnWidth(1), // Qty
2: pw.FlexColumnWidth(2.5), // Pendapatan
3: pw.FlexColumnWidth(2), // HPP
4: pw.FlexColumnWidth(2), // Laba Kotor
5: pw.FlexColumnWidth(2), // Margin (%)
},
children: profitLossData?.productData
.map(
(profitLoss) => _buildPerProductDataRow(
product: profitLoss.productName,
qty: profitLoss.quantitySold.toString(),
pendapatan: profitLoss.revenue
.toString()
.currencyFormatRpV2,
hpp: profitLoss.cost
.toString()
.currencyFormatRpV2,
labaKotor: profitLoss.grossProfit
.toString()
.currencyFormatRpV2,
margin:
'${profitLoss.grossProfitMargin.round()}%',
isEven: profitLossData.productData
.indexOf(profitLoss) %
2 ==
0,
),
)
.toList() ??
[],
),
),
pw.Container(
decoration: pw.BoxDecoration(
color: primaryColor, // Purple color
borderRadius: pw.BorderRadius.only(
bottomLeft: pw.Radius.circular(8),
bottomRight: pw.Radius.circular(8),
),
),
child: pw.Table(
columnWidths: const {
0: pw.FlexColumnWidth(2.5), // Produk
1: pw.FlexColumnWidth(1), // Qty
2: pw.FlexColumnWidth(2.5), // Pendapatan
3: pw.FlexColumnWidth(2), // HPP
4: pw.FlexColumnWidth(2), // Laba Kotor
5: pw.FlexColumnWidth(2), // Margin (%)
},
children: [
pw.TableRow(
children: [
_buildTotalCell('TOTAL'),
_buildTotalCell(
profitLossProductSummary['totalQuantity']
.toString()),
_buildTotalCell(
profitLossProductSummary['totalRevenue']
.toString()
.currencyFormatRpV2),
_buildTotalCell(
profitLossProductSummary['totalCost']
.toString()
.currencyFormatRpV2),
_buildTotalCell(
profitLossProductSummary['totalGrossProfit']
.toString()
.currencyFormatRpV2),
_buildTotalCell(''),
],
),
],
),
),
],
),
],
),
),
// Summary Payment Method
pw.Container(
padding: pw.EdgeInsets.all(20),
child: pw.Column(
children: [
pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
_buildSectionWidget('2. Ringkasan Metode Pembayaran'),
pw.SizedBox(height: 30),
pw.Container(
decoration: pw.BoxDecoration(
color: primaryColor, // Purple color
borderRadius: pw.BorderRadius.only(
topLeft: pw.Radius.circular(8),
topRight: pw.Radius.circular(8),
),
),
child: pw.Table(
columnWidths: const {
0: pw.FlexColumnWidth(2.5), // Nama
1: pw.FlexColumnWidth(1), // Tipe
2: pw.FlexColumnWidth(2.5), // Jumlah Order
3: pw.FlexColumnWidth(2), // Total Amount
4: pw.FlexColumnWidth(2), // Presentase
},
children: [
pw.TableRow(
children: [
_buildHeaderCell('Nama'),
_buildHeaderCell('Tipe'),
_buildHeaderCell('Jumlah Order'),
_buildHeaderCell('Total Amount'),
_buildHeaderCell('Presentase'),
],
),
],
),
),
pw.Container(
decoration: pw.BoxDecoration(
color: PdfColors.white,
),
child: pw.Table(
columnWidths: {
0: pw.FlexColumnWidth(2.5), // Nama
1: pw.FlexColumnWidth(1), // Tipe
2: pw.FlexColumnWidth(2.5), // Jumlah Order
3: pw.FlexColumnWidth(2), // Total Amount
4: pw.FlexColumnWidth(2), // Presentase
},
children: paymentMethodAnalyticData?.data
.map(
(payment) => _buildPaymentMethodDataRow(
name: payment.paymentMethodName,
tipe: payment.paymentMethodType
.toTitleCase(),
jumlahOrder:
payment.orderCount.toString(),
totalAmount: payment.totalAmount
.toString()
.currencyFormatRpV2,
presentase:
'${payment.percentage.round()}%',
isEven: paymentMethodAnalyticData.data
.indexOf(payment) %
2 ==
0,
),
)
.toList() ??
[],
),
),
pw.Container(
decoration: pw.BoxDecoration(
color: primaryColor, // Purple color
borderRadius: pw.BorderRadius.only(
bottomLeft: pw.Radius.circular(8),
bottomRight: pw.Radius.circular(8),
),
),
child: pw.Table(
columnWidths: const {
0: pw.FlexColumnWidth(2.5), // Produk
1: pw.FlexColumnWidth(1), // Qty
2: pw.FlexColumnWidth(2.5), // Pendapatan
3: pw.FlexColumnWidth(2), // HPP
4: pw.FlexColumnWidth(2), // Laba Kotor
5: pw.FlexColumnWidth(2), // Margin (%)
},
children: [
pw.TableRow(
children: [
_buildTotalCell('TOTAL'),
_buildTotalCell(''),
_buildTotalCell((paymentMethodAnalyticData
?.summary.totalOrders ??
0)
.toString()),
_buildTotalCell((paymentMethodAnalyticData
?.summary.totalAmount ??
0)
.toString()
.currencyFormatRpV2),
_buildTotalCell(''),
],
),
],
),
),
],
),
],
),
),
// Summary Category
pw.Container(
padding: pw.EdgeInsets.all(20),
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
_buildSectionWidget('3. Ringkasan Kategori'),
pw.SizedBox(height: 30),
pw.Column(
children: [
pw.Container(
decoration: pw.BoxDecoration(
color: primaryColor, // Purple color
borderRadius: pw.BorderRadius.only(
topLeft: pw.Radius.circular(8),
topRight: pw.Radius.circular(8),
),
),
child: pw.Table(
columnWidths: const {
0: pw.FlexColumnWidth(2.5), // Nama
1: pw.FlexColumnWidth(2), // Total Product
2: pw.FlexColumnWidth(1), // qty
3: pw.FlexColumnWidth(2), // Jumlah Order
4: pw.FlexColumnWidth(2.5), // Presentase
},
children: [
pw.TableRow(
children: [
_buildHeaderCell('Nama'),
_buildHeaderCell('Total Produk'),
_buildHeaderCell('Qty'),
_buildHeaderCell('Jumlah Order'),
_buildHeaderCell('Pendapatan'),
],
),
],
),
),
pw.Container(
decoration: pw.BoxDecoration(
color: PdfColors.white,
),
child: pw.Table(
columnWidths: {
0: pw.FlexColumnWidth(2.5), // Nama
1: pw.FlexColumnWidth(2), // Total Product
2: pw.FlexColumnWidth(1), // qty
3: pw.FlexColumnWidth(2), // Jumlah Order
4: pw.FlexColumnWidth(2.5), // Presentase
},
children: categoryAnalyticData?.data
.map((category) => _buildCategoryDataRow(
name: category.categoryName,
totalProduct:
category.productCount.toString(),
qty: category.totalQuantity.toString(),
jumlahOrder:
category.orderCount.toString(),
pendapatan: category.totalRevenue
.toString()
.currencyFormatRpV2,
isEven: categoryAnalyticData.data
.indexOf(category) %
2 ==
0,
))
.toList() ??
[],
),
),
pw.Container(
decoration: pw.BoxDecoration(
color: primaryColor, // Purple color
borderRadius: pw.BorderRadius.only(
bottomLeft: pw.Radius.circular(8),
bottomRight: pw.Radius.circular(8),
),
),
child: pw.Table(
columnWidths: const {
0: pw.FlexColumnWidth(2.5), // Nama
1: pw.FlexColumnWidth(2), // Total Product
2: pw.FlexColumnWidth(1), // qty
3: pw.FlexColumnWidth(2), // Jumlah Order
4: pw.FlexColumnWidth(2.5), // Presentase
},
children: [
pw.TableRow(
children: [
_buildTotalCell('TOTAL'),
_buildTotalCell(
categorySummary['productCount'].toString()),
_buildTotalCell(categorySummary['totalQuantity']
.toString()),
_buildTotalCell(
categorySummary['orderCount'].toString()),
_buildTotalCell(categorySummary['totalRevenue']
.toString()
.currencyFormatRpV2),
],
),
],
),
),
],
),
],
),
),
// Summary Item
pw.Container(
padding: pw.EdgeInsets.all(20),
child: pw.Column(
children: [
pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
_buildSectionWidget('4. Ringkasan Item'),
pw.SizedBox(height: 30),
pw.Container(
decoration: pw.BoxDecoration(
color: primaryColor, // Purple color
borderRadius: pw.BorderRadius.only(
topLeft: pw.Radius.circular(8),
topRight: pw.Radius.circular(8),
),
),
child: pw.Table(
columnWidths: const {
0: pw.FlexColumnWidth(2.5), // Produk
1: pw.FlexColumnWidth(2), // Kategori
2: pw.FlexColumnWidth(1), // qty
3: pw.FlexColumnWidth(2), // Order
4: pw.FlexColumnWidth(2), // Pendapatan
5: pw.FlexColumnWidth(2), // Average
},
children: [
pw.TableRow(
children: [
_buildHeaderCell('Produk'),
_buildHeaderCell('Kategori'),
_buildHeaderCell('Qty'),
_buildHeaderCell('Order'),
_buildHeaderCell('Pendapatan'),
_buildHeaderCell('Rata Rata'),
],
),
],
),
),
pw.Container(
decoration: pw.BoxDecoration(
color: PdfColors.white,
),
child: pw.Table(
columnWidths: {
0: pw.FlexColumnWidth(2.5), // Produk
1: pw.FlexColumnWidth(2), // Kategori
2: pw.FlexColumnWidth(1), // qty
3: pw.FlexColumnWidth(2), // Order
4: pw.FlexColumnWidth(2), // Pendapatan
5: pw.FlexColumnWidth(2), // Average
},
children: productAnalyticData?.data
.map((item) => _buildItemDataRow(
product: item.productName,
category: item.categoryName,
qty: item.quantitySold.toString(),
order: item.orderCount.toString(),
pendapatan: item.revenue
.toString()
.currencyFormatRpV2,
average: item.averagePrice
.round()
.toString()
.currencyFormatRpV2,
isEven: productAnalyticData.data
.indexOf(item) %
2 ==
0,
))
.toList() ??
[],
),
),
pw.Container(
decoration: pw.BoxDecoration(
color: primaryColor, // Purple color
borderRadius: pw.BorderRadius.only(
bottomLeft: pw.Radius.circular(8),
bottomRight: pw.Radius.circular(8),
),
),
child: pw.Table(
columnWidths: const {
0: pw.FlexColumnWidth(2.5), // Produk
1: pw.FlexColumnWidth(2), // Kategori
2: pw.FlexColumnWidth(1), // qty
3: pw.FlexColumnWidth(2), // Order
4: pw.FlexColumnWidth(2), // Pendapatan
5: pw.FlexColumnWidth(2), // Average
},
children: [
pw.TableRow(
children: [
_buildTotalCell('TOTAL'),
_buildTotalCell(''),
_buildTotalCell(
productItemSummary['totalQuantitySold']
.toString()),
_buildTotalCell(productItemSummary['orderCount']
.toString()),
_buildTotalCell(
productItemSummary['totalRevenue']
.toString()
.currencyFormatRpV2),
_buildTotalCell(''),
],
),
],
),
),
],
),
],
),
),
];
},
),
);
return HelperPdfService.saveDocument(
name: 'Laporan Transaksi | $searchDateFormatted.pdf', pdf: pdf);
}
static pw.Widget _buildSectionWidget(String title) {
return pw.Text(
title,
style: pw.TextStyle(
fontSize: 20,
fontWeight: pw.FontWeight.bold,
color: primaryColor,
),
);
}
static pw.Widget _buildSummaryItem(
String label,
String value, {
pw.TextStyle? valueStyle,
pw.TextStyle? labelStyle,
}) {
return pw.Container(
padding: pw.EdgeInsets.only(bottom: 8),
margin: pw.EdgeInsets.only(bottom: 16),
decoration: pw.BoxDecoration(
border: pw.Border(
bottom: pw.BorderSide(
color: PdfColors.grey300,
),
),
),
child: pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
children: [
pw.Text(label, style: labelStyle),
pw.Text(
value,
style: valueStyle ??
pw.TextStyle(
fontWeight: pw.FontWeight.bold,
),
),
],
),
);
}
static pw.Widget _buildHeaderCell(String text) {
return pw.Container(
padding: pw.EdgeInsets.symmetric(horizontal: 12, vertical: 16),
child: pw.Text(
text,
style: pw.TextStyle(
color: PdfColors.white,
fontWeight: pw.FontWeight.bold,
fontSize: 12,
),
textAlign: pw.TextAlign.center,
),
);
}
static pw.Widget _buildDataCell(String text,
{pw.Alignment alignment = pw.Alignment.center, PdfColor? textColor}) {
return pw.Container(
padding: pw.EdgeInsets.symmetric(horizontal: 12, vertical: 16),
alignment: alignment,
child: pw.Text(
text,
style: pw.TextStyle(
fontSize: 12,
color: textColor ?? PdfColors.black,
fontWeight: pw.FontWeight.normal,
),
textAlign: alignment == pw.Alignment.centerLeft
? pw.TextAlign.left
: pw.TextAlign.center,
),
);
}
static pw.Widget _buildTotalCell(String text) {
return pw.Container(
padding: pw.EdgeInsets.symmetric(horizontal: 12, vertical: 16),
child: pw.Text(
text,
style: pw.TextStyle(
color: PdfColors.white,
fontWeight: pw.FontWeight.bold,
fontSize: 12,
),
textAlign: pw.TextAlign.center,
),
);
}
static pw.TableRow _buildPerProductDataRow({
required String product,
required String qty,
required String pendapatan,
required String hpp,
required String labaKotor,
required String margin,
required bool isEven,
}) {
return pw.TableRow(
decoration: pw.BoxDecoration(
color: isEven ? PdfColors.grey50 : PdfColors.white,
),
children: [
_buildDataCell(product, alignment: pw.Alignment.centerLeft),
_buildDataCell(qty),
_buildDataCell(pendapatan),
_buildDataCell(hpp, textColor: PdfColors.red600),
_buildDataCell(labaKotor, textColor: PdfColors.green600),
_buildDataCell(margin),
],
);
}
static pw.TableRow _buildPaymentMethodDataRow({
required String name,
required String tipe,
required String jumlahOrder,
required String totalAmount,
required String presentase,
required bool isEven,
}) {
return pw.TableRow(
decoration: pw.BoxDecoration(
color: isEven ? PdfColors.grey50 : PdfColors.white,
),
children: [
_buildDataCell(name, alignment: pw.Alignment.centerLeft),
_buildDataCell(tipe),
_buildDataCell(jumlahOrder),
_buildDataCell(totalAmount),
_buildDataCell(presentase),
],
);
}
static pw.TableRow _buildCategoryDataRow({
required String name,
required String totalProduct,
required String qty,
required String jumlahOrder,
required String pendapatan,
required bool isEven,
}) {
return pw.TableRow(
decoration: pw.BoxDecoration(
color: isEven ? PdfColors.grey50 : PdfColors.white,
),
children: [
_buildDataCell(name, alignment: pw.Alignment.centerLeft),
_buildDataCell(totalProduct),
_buildDataCell(qty),
_buildDataCell(jumlahOrder),
_buildDataCell(pendapatan),
],
);
}
static pw.TableRow _buildItemDataRow({
required String product,
required String category,
required String qty,
required String order,
required String pendapatan,
required String average,
required bool isEven,
}) {
return pw.TableRow(
decoration: pw.BoxDecoration(
color: isEven ? PdfColors.grey50 : PdfColors.white,
),
children: [
_buildDataCell(product, alignment: pw.Alignment.centerLeft),
_buildDataCell(category, alignment: pw.Alignment.centerLeft),
_buildDataCell(qty),
_buildDataCell(order),
_buildDataCell(pendapatan),
_buildDataCell(average),
],
);
}
}
+722 -61
View File
@@ -534,7 +534,7 @@ class PrintDataoutputs {
styles: const PosStyles(align: PosAlign.left), styles: const PosStyles(align: PosAlign.left),
), ),
PosColumn( PosColumn(
text: 'JF-${DateFormat('yyyyMMddhhmm').format(DateTime.now())}', text: '${DateFormat('yyyyMMddhhmm').format(DateTime.now())}',
width: 6, width: 6,
styles: const PosStyles(align: PosAlign.right), styles: const PosStyles(align: PosAlign.right),
), ),
@@ -751,6 +751,7 @@ class PrintDataoutputs {
int paper, int paper,
String orderType, String orderType,
Outlet outlet, Outlet outlet,
List<ProductQuantity> products,
) async { ) async {
List<int> bytes = []; List<int> bytes = [];
@@ -798,7 +799,7 @@ class PrintDataoutputs {
styles: const PosStyles(align: PosAlign.left), styles: const PosStyles(align: PosAlign.left),
), ),
PosColumn( PosColumn(
text: 'JF-${DateFormat('yyyyMMddhhmm').format(DateTime.now())}', text: DateFormat('yyyyMMddhhmm').format(DateTime.now()),
width: 6, width: 6,
styles: const PosStyles(align: PosAlign.right), styles: const PosStyles(align: PosAlign.right),
), ),
@@ -840,9 +841,10 @@ class PrintDataoutputs {
styles: const PosStyles(align: PosAlign.right), styles: const PosStyles(align: PosAlign.right),
), ),
]); ]);
if (paymentMethod != '') {
bytes += generator.row([ bytes += generator.row([
PosColumn( PosColumn(
text: 'Pembayaran', text: 'Payment',
width: 8, width: 8,
styles: const PosStyles(align: PosAlign.left), styles: const PosStyles(align: PosAlign.left),
), ),
@@ -852,6 +854,7 @@ class PrintDataoutputs {
styles: const PosStyles(align: PosAlign.right), styles: const PosStyles(align: PosAlign.right),
), ),
]); ]);
}
bytes += generator.text( bytes += generator.text(
paper == 80 paper == 80
@@ -865,19 +868,34 @@ class PrintDataoutputs {
? '------------------------------------------------' ? '------------------------------------------------'
: '--------------------------------', : '--------------------------------',
styles: const PosStyles(bold: false, align: PosAlign.center)); styles: const PosStyles(bold: false, align: PosAlign.center));
for (final product in (order.orderItems ?? <OrderItem>[])) { for (final product in (products)) {
bytes += generator.row([ bytes += generator.row([
PosColumn( PosColumn(
text: '${product.quantity} x ${product.productName}', text: '${product.quantity} x ${product.product.name}',
width: 8, width: 8,
styles: const PosStyles(bold: true, align: PosAlign.left), styles: const PosStyles(bold: true, align: PosAlign.left),
), ),
PosColumn( PosColumn(
text: (product.totalPrice ?? 0).currencyFormatRpV2, text: (((product.product.price ?? 0) * product.quantity))
.currencyFormatRpV2,
width: 4, width: 4,
styles: const PosStyles(bold: true, align: PosAlign.right), styles: const PosStyles(bold: true, align: PosAlign.right),
), ),
]); ]);
if (product.notes != '') {
bytes += generator.row([
PosColumn(
text: 'Note',
width: 4,
styles: const PosStyles(bold: false, align: PosAlign.left),
),
PosColumn(
text: product.notes,
width: 8,
styles: const PosStyles(bold: false, align: PosAlign.right),
),
]);
}
} }
bytes += generator.text( bytes += generator.text(
paper == 80 paper == 80
@@ -887,7 +905,7 @@ class PrintDataoutputs {
bytes += generator.row([ bytes += generator.row([
PosColumn( PosColumn(
text: 'Subtotal ${order.orderItems?.length ?? "0"} Product', text: 'Subtotal ${products.length} Product',
width: 6, width: 6,
styles: const PosStyles(align: PosAlign.left), styles: const PosStyles(align: PosAlign.left),
), ),
@@ -1000,6 +1018,239 @@ class PrintDataoutputs {
return bytes; return bytes;
} }
Future<List<int>> printVoidOrder(
Order order,
String chashierName,
int taxPercentage,
int paper,
int nominalDikembalikan,
Outlet outlet,
List<OrderItem> productItemVoid,
) 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: '${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: 'Status',
width: 8,
styles: const PosStyles(align: PosAlign.left),
),
PosColumn(
text: 'Dibatalkan',
width: 4,
styles: const PosStyles(align: PosAlign.right),
),
]);
for (final product in (productItemVoid)) {
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 ${productItemVoid.length} 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: 'Dikembalikan',
width: 6,
styles: const PosStyles(align: PosAlign.left),
),
PosColumn(
text: nominalDikembalikan.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 = [];
@@ -1040,6 +1291,7 @@ class PrintDataoutputs {
String tableName, String tableName,
String draftName, String draftName,
String cashierName, String cashierName,
String customerName,
int paper, int paper,
String orderType) async { String orderType) async {
List<int> bytes = []; List<int> bytes = [];
@@ -1090,14 +1342,11 @@ class PrintDataoutputs {
styles: const PosStyles(align: PosAlign.left), styles: const PosStyles(align: PosAlign.left),
), ),
PosColumn( PosColumn(
text: 'JF-${DateFormat('yyyyMMddhhmm').format(DateTime.now())}', text: '${DateFormat('yyyyMMddhhmm').format(DateTime.now())}',
width: 6, width: 6,
styles: const PosStyles(align: PosAlign.right), styles: const PosStyles(align: PosAlign.right),
), ),
]); ]);
// bytes += generator.text(
// 'Receipt: JF-${DateFormat('yyyyMMddhhmm').format(DateTime.now())}',
// styles: const PosStyles(bold: false, align: PosAlign.left));
//cashier name //cashier name
bytes += generator.row([ bytes += generator.row([
PosColumn( PosColumn(
@@ -1117,7 +1366,7 @@ class PrintDataoutputs {
//column 2 //column 2
bytes += generator.row([ bytes += generator.row([
PosColumn( PosColumn(
text: 'Customer - $draftName', text: '$customerName - $draftName',
width: 6, width: 6,
styles: const PosStyles(align: PosAlign.left), styles: const PosStyles(align: PosAlign.left),
), ),
@@ -1163,18 +1412,182 @@ class PrintDataoutputs {
styles: const PosStyles(bold: false, align: PosAlign.center)); styles: const PosStyles(bold: false, align: PosAlign.center));
paper == 80 ? bytes += generator.feed(3) : bytes += generator.feed(1); paper == 80 ? bytes += generator.feed(3) : bytes += generator.feed(1);
//cut //cut
if (paper == 80) {
bytes += generator.cut(); bytes += generator.cut();
}
return bytes; return bytes;
} }
Future<List<int>> printKitchen( Future<List<int>> printBar(
List<ProductQuantity> products, List<ProductQuantity> products,
String tableNumber, String tableNumber,
String draftName, String draftName,
String cashierName, String cashierName,
String customerName,
int paper,
String orderType,
) 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('Table Bar',
styles: const PosStyles(
bold: true,
align: PosAlign.center,
height: PosTextSize.size2,
width: PosTextSize.size2,
));
bytes += generator.feed(1);
if (tableNumber.isNotEmpty) {
bytes += generator.text(tableNumber.toString(),
styles: const PosStyles(
bold: true,
align: PosAlign.center,
height: PosTextSize.size2,
width: PosTextSize.size2,
));
bytes += generator.feed(1);
}
bytes += generator.row([
PosColumn(
text: 'Date',
width: 6,
styles: const PosStyles(align: PosAlign.left),
),
PosColumn(
text: DateFormat('dd-MM-yyyy HH:mm').format(DateTime.now()),
width: 6,
styles: const PosStyles(align: PosAlign.right),
),
]);
// bytes += generator.text(
// 'Date: ${DateFormat('dd-MM-yyyy HH:mm').format(DateTime.now())}',
// styles: const PosStyles(bold: false, align: PosAlign.left));
//reciept number
bytes += generator.row([
PosColumn(
text: 'Receipt',
width: 6,
styles: const PosStyles(align: PosAlign.left),
),
PosColumn(
text: DateFormat('yyyyMMddhhmm').format(DateTime.now()),
width: 6,
styles: const PosStyles(align: PosAlign.right),
),
]);
bytes += generator.row([
PosColumn(
text: 'Cashier',
width: 6,
styles: const PosStyles(align: PosAlign.left),
),
PosColumn(
text: cashierName,
width: 6,
styles: const PosStyles(align: PosAlign.right),
),
]);
bytes += generator.row([
PosColumn(
text: '$customerName - $draftName',
width: 6,
styles: const PosStyles(align: PosAlign.left),
),
PosColumn(
text: orderType,
width: 6,
styles: const PosStyles(align: PosAlign.right, bold: true),
),
]);
//----
bytes += generator.text(
paper == 80
? '------------------------------------------------'
: '--------------------------------',
styles: const PosStyles(bold: false, align: PosAlign.center));
bytes += generator.feed(1);
final barProducts = products.where((p) => p.product.printerType == 'bar');
for (final product in barProducts) {
bytes += generator.text('${product.quantity} x ${product.product.name}',
styles: const PosStyles(
align: PosAlign.left,
bold: false,
height: PosTextSize.size2,
width: PosTextSize.size1,
));
if (product.notes.isNotEmpty) {
bytes += generator.text(' Notes: ${product.notes}',
styles: const PosStyles(
align: PosAlign.left,
bold: false,
height: PosTextSize.size1,
width: PosTextSize.size1,
fontType: PosFontType.fontA,
));
}
}
bytes += generator.feed(1);
bytes += generator.text(
paper == 80
? '------------------------------------------------'
: '--------------------------------',
styles: const PosStyles(bold: false, align: PosAlign.center));
paper == 80 ? bytes += generator.feed(3) : bytes += generator.feed(1);
//cut
bytes += generator.cut();
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;
}
Future<List<int>> printKitchenAllItem(
List<ProductQuantity> products,
String tableNumber,
String draftName,
String cashierName,
String customerName,
int paper, int paper,
String orderType) async { String orderType) async {
List<int> bytes = []; List<int> bytes = [];
@@ -1227,14 +1640,26 @@ class PrintDataoutputs {
styles: const PosStyles(align: PosAlign.left), styles: const PosStyles(align: PosAlign.left),
), ),
PosColumn( PosColumn(
text: 'JF-${DateFormat('yyyyMMddhhmm').format(DateTime.now())}', text: DateFormat('yyyyMMddhhmm').format(DateTime.now()),
width: 6, width: 6,
styles: const PosStyles(align: PosAlign.right), styles: const PosStyles(align: PosAlign.right),
), ),
]); ]);
bytes += generator.row([ bytes += generator.row([
PosColumn( PosColumn(
text: 'Customer - $draftName', text: 'Cashier',
width: 6,
styles: const PosStyles(align: PosAlign.left),
),
PosColumn(
text: cashierName,
width: 6,
styles: const PosStyles(align: PosAlign.right),
),
]);
bytes += generator.row([
PosColumn(
text: '$customerName - $draftName',
width: 6, width: 6,
styles: const PosStyles(align: PosAlign.left), styles: const PosStyles(align: PosAlign.left),
), ),
@@ -1281,25 +1706,32 @@ class PrintDataoutputs {
: '--------------------------------', : '--------------------------------',
styles: const PosStyles(bold: false, align: PosAlign.center)); styles: const PosStyles(bold: false, align: PosAlign.center));
paper == 80 ? bytes += generator.feed(3) : bytes += generator.feed(1); paper == 80 ? bytes += generator.feed(3) : bytes += generator.feed(1);
//cut
if (paper == 80) {
bytes += generator.cut(); bytes += generator.cut();
}
return bytes; return bytes;
} }
Future<List<int>> printBar(List<ProductQuantity> products, String tableNumber, Future<List<int>> printKitchenPerProduct(
String draftName, String cashierName, int paper, String orderType) async { List<ProductQuantity> products,
List<int> bytes = []; String tableNumber,
String draftName,
String cashierName,
String customerName,
int paper,
String orderType) async {
List<int> allBytes = [];
final profile = await CapabilityProfile.load(); final profile = await CapabilityProfile.load();
final generator = final generator =
Generator(paper == 58 ? PaperSize.mm58 : PaperSize.mm80, profile); Generator(paper == 58 ? PaperSize.mm58 : PaperSize.mm80, profile);
// Loop untuk setiap produk - print terpisah
for (final product in products) {
List<int> bytes = [];
bytes += generator.reset(); bytes += generator.reset();
bytes += generator.text('Table Bar', bytes += generator.text('Table Kitchen',
styles: const PosStyles( styles: const PosStyles(
bold: true, bold: true,
align: PosAlign.center, align: PosAlign.center,
@@ -1307,8 +1739,9 @@ class PrintDataoutputs {
width: PosTextSize.size2, width: PosTextSize.size2,
)); ));
bytes += generator.feed(1); bytes += generator.feed(1);
if (tableNumber.isNotEmpty) { if (tableNumber.isNotEmpty) {
bytes += generator.text(tableNumber.toString(), bytes += generator.text(tableNumber,
styles: const PosStyles( styles: const PosStyles(
bold: true, bold: true,
align: PosAlign.center, align: PosAlign.center,
@@ -1320,35 +1753,33 @@ class PrintDataoutputs {
bytes += generator.row([ bytes += generator.row([
PosColumn( PosColumn(
text: 'Date', text: DateFormat('dd MMM yyyy').format(DateTime.now()),
width: 6, width: 6,
styles: const PosStyles(align: PosAlign.left), styles: const PosStyles(align: PosAlign.left),
), ),
PosColumn( PosColumn(
text: DateFormat('dd-MM-yyyy HH:mm').format(DateTime.now()), text: DateFormat('HH:mm').format(DateTime.now()),
width: 6, width: 6,
styles: const PosStyles(align: PosAlign.right), styles: const PosStyles(align: PosAlign.right),
), ),
]); ]);
// bytes += generator.text(
// 'Date: ${DateFormat('dd-MM-yyyy HH:mm').format(DateTime.now())}',
// styles: const PosStyles(bold: false, align: PosAlign.left));
//reciept number
bytes += generator.row([ bytes += generator.row([
PosColumn( PosColumn(
text: 'Receipt', text: draftName,
width: 6, width: 8,
styles: const PosStyles(align: PosAlign.left), styles: const PosStyles(align: PosAlign.left),
), ),
PosColumn( PosColumn(
text: 'JF-${DateFormat('yyyyMMddhhmm').format(DateTime.now())}', text: '',
width: 6, width: 4,
styles: const PosStyles(align: PosAlign.right), styles: const PosStyles(align: PosAlign.right),
), ),
]); ]);
bytes += generator.row([ bytes += generator.row([
PosColumn( PosColumn(
text: 'Customer - $draftName', text: customerName,
width: 6, width: 6,
styles: const PosStyles(align: PosAlign.left), styles: const PosStyles(align: PosAlign.left),
), ),
@@ -1359,15 +1790,14 @@ class PrintDataoutputs {
), ),
]); ]);
//----
bytes += generator.text( bytes += generator.text(
paper == 80 paper == 80
? '------------------------------------------------' ? '------------------------------------------------'
: '--------------------------------', : '--------------------------------',
styles: const PosStyles(bold: false, align: PosAlign.center)); styles: const PosStyles(bold: false, align: PosAlign.center));
bytes += generator.feed(1); bytes += generator.feed(1);
final barProducts = products.where((p) => p.product.printerType == 'bar');
for (final product in barProducts) { // Print hanya 1 produk per struk
bytes += generator.text('${product.quantity} x ${product.product.name}', bytes += generator.text('${product.quantity} x ${product.product.name}',
styles: const PosStyles( styles: const PosStyles(
align: PosAlign.left, align: PosAlign.left,
@@ -1375,6 +1805,7 @@ class PrintDataoutputs {
height: PosTextSize.size2, height: PosTextSize.size2,
width: PosTextSize.size1, width: PosTextSize.size1,
)); ));
if (product.notes.isNotEmpty) { if (product.notes.isNotEmpty) {
bytes += generator.text(' Notes: ${product.notes}', bytes += generator.text(' Notes: ${product.notes}',
styles: const PosStyles( styles: const PosStyles(
@@ -1385,7 +1816,6 @@ class PrintDataoutputs {
fontType: PosFontType.fontA, fontType: PosFontType.fontA,
)); ));
} }
}
bytes += generator.feed(1); bytes += generator.feed(1);
bytes += generator.text( bytes += generator.text(
@@ -1393,47 +1823,278 @@ class PrintDataoutputs {
? '------------------------------------------------' ? '------------------------------------------------'
: '--------------------------------', : '--------------------------------',
styles: const PosStyles(bold: false, align: PosAlign.center)); styles: const PosStyles(bold: false, align: PosAlign.center));
paper == 80 ? bytes += generator.feed(3) : bytes += generator.feed(1);
//cut paper == 80 ? bytes += generator.feed(3) : bytes += generator.feed(2);
if (paper == 80) {
bytes += generator.cut(); bytes += generator.cut();
// Tambahkan ke allBytes
allBytes += bytes;
} }
return bytes; return allBytes;
} }
Future<List<int>> printTicket( Future<List<int>> printSplitBill(
int totalPrice, Uint8List imageQris, int paper) async { Order order,
String chashierName,
int paper,
Outlet outlet,
) async {
List<int> bytes = []; List<int> bytes = [];
final profile = await CapabilityProfile.load(); final profile = await CapabilityProfile.load();
final generator = final generator =
Generator(paper == 58 ? PaperSize.mm58 : PaperSize.mm80, profile); Generator(paper == 58 ? PaperSize.mm58 : PaperSize.mm80, profile);
final img.Image? orginalImage = img.decodeImage(imageQris);
bytes += generator.reset(); bytes += generator.reset();
// final Uint8List bytesData = data.buffer.asUint8List(); bytes += generator.text(outlet.name ?? "",
// final img.Image? orginalImage = img.decodeImage(bytesData); styles: const PosStyles(
// bytes += generator.reset(); bold: true,
align: PosAlign.center,
height: PosTextSize.size1,
width: PosTextSize.size1,
));
bytes += generator.text('Scan Ticket', bytes += generator.text(outlet.address ?? "",
styles: const PosStyles(bold: false, align: PosAlign.center)); styles: const PosStyles(bold: false, align: PosAlign.center));
bytes += generator.feed(2); bytes += generator.text(outlet.phoneNumber ?? "",
if (orginalImage != null) { styles: const PosStyles(bold: false, align: PosAlign.center));
final img.Image grayscalledImage = img.grayscale(orginalImage);
final img.Image resizedImage = bytes += generator.text(
img.copyResize(grayscalledImage, width: 240); paper == 80
bytes += generator.imageRaster(resizedImage, align: PosAlign.center); ? '------------------------------------------------'
bytes += generator.feed(1); : '--------------------------------',
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: 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),
),
]);
if (order.payments != null) {
bytes += generator.row([
PosColumn(
text: 'Payment',
width: 8,
styles: const PosStyles(align: PosAlign.left),
),
PosColumn(
text: order.payments?.last.paymentMethodName ?? '-',
width: 4,
styles: const PosStyles(align: PosAlign.right),
),
]);
} }
bytes += generator.text('Price : ${totalPrice.currencyFormatRp}', bytes += generator.text(
paper == 80
? '------------------------------------------------'
: '--------------------------------',
styles: const PosStyles(bold: false, align: PosAlign.center));
bytes += generator.text(order.orderType ?? '-',
styles: const PosStyles(bold: true, align: PosAlign.center));
bytes += generator.text(
paper == 80
? '------------------------------------------------'
: '--------------------------------',
styles: const PosStyles(bold: false, align: PosAlign.center));
bytes += generator.text("SPLIT",
styles: const PosStyles(bold: true, align: PosAlign.center));
bytes += generator.text(
paper == 80
? '------------------------------------------------'
: '--------------------------------',
styles: const PosStyles(bold: false, align: PosAlign.center)); styles: const PosStyles(bold: false, align: PosAlign.center));
bytes += generator.feed(4); for (final item in (order.orderItems ?? <OrderItem>[])) {
bytes += generator.cut(); bytes += generator.row([
PosColumn(
text: '${item.quantity} x ${item.productName}',
width: 8,
styles: const PosStyles(bold: true, align: PosAlign.left),
),
PosColumn(
text: (((item.unitPrice ?? 0) * (item.quantity ?? 0)))
.currencyFormatRpV2,
width: 4,
styles: const PosStyles(bold: true, align: PosAlign.right),
),
]);
if (item.notes != '') {
bytes += generator.row([
PosColumn(
text: 'Note',
width: 4,
styles: const PosStyles(bold: false, align: PosAlign.left),
),
PosColumn(
text: item.notes ?? "-",
width: 8,
styles: const PosStyles(bold: false, align: PosAlign.right),
),
]);
}
}
if (order.orderItems?.isNotEmpty ?? false) {
bytes += generator.text(
paper == 80
? '------------------------------------------------'
: '--------------------------------',
styles: const PosStyles(bold: false, align: PosAlign.center));
}
bytes += generator.row([
PosColumn(
text: 'Subtotal',
width: 6,
styles: const PosStyles(align: PosAlign.left),
),
PosColumn(
text: order.payments?.last.amount?.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 (${order.taxAmount}%)',
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.payments?.last.amount?.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: order.payments?.last.amount?.currencyFormatRpV2 ?? '-',
width: 6,
styles: const PosStyles(align: PosAlign.right),
),
]);
bytes += generator.text(
paper == 80
? '------------------------------------------------'
: '--------------------------------',
styles: const PosStyles(bold: false, align: PosAlign.center));
paper == 80 ? bytes += generator.feed(3) : bytes += generator.feed(1);
bytes += generator.cut();
return bytes; return bytes;
} }
} }
@@ -5,7 +5,9 @@ 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/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_analytic_response_model.dart';
import 'package:enaklo_pos/data/models/response/dashboard_analytic_response_model.dart'; import 'package:enaklo_pos/data/models/response/dashboard_analytic_response_model.dart';
import 'package:enaklo_pos/data/models/response/inventory_analytic_response_model.dart';
import 'package:enaklo_pos/data/models/response/payment_method_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/product_analytic_response_model.dart';
import 'package:enaklo_pos/data/models/response/profit_loss_response_model.dart'; import 'package:enaklo_pos/data/models/response/profit_loss_response_model.dart';
@@ -184,4 +186,72 @@ class AnalyticRemoteDatasource {
return left('Unexpected error occurred'); return left('Unexpected error occurred');
} }
} }
Future<Either<String, CategoryAnalyticResponseModel>> getCategory({
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/categories',
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(CategoryAnalyticResponseModel.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, InventoryAnalyticResponseModel>> getInventory({
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/inventory/report/details/${authData.user?.outletId}',
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(InventoryAnalyticResponseModel.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');
}
}
} }
@@ -0,0 +1,373 @@
import 'dart:convert';
import 'dart:developer';
import 'package:enaklo_pos/core/database/database_handler.dart';
import 'package:enaklo_pos/data/models/response/category_response_model.dart';
import 'package:sqflite/sqflite.dart';
class CategoryLocalDatasource {
static CategoryLocalDatasource? _instance;
CategoryLocalDatasource._internal();
static CategoryLocalDatasource get instance {
_instance ??= CategoryLocalDatasource._internal();
return _instance!;
}
Future<Database> get _db async => await DatabaseHelper.instance.database;
// ========================================
// CACHING SYSTEM
// ========================================
final Map<String, List<CategoryModel>> _queryCache = {};
final Duration _cacheExpiry =
Duration(minutes: 10); // Lebih lama untuk categories
final Map<String, DateTime> _cacheTimestamps = {};
// ========================================
// BATCH SAVE CATEGORIES
// ========================================
Future<void> saveCategoriesBatch(List<CategoryModel> categories,
{bool clearFirst = false}) async {
final db = await _db;
try {
await db.transaction((txn) async {
if (clearFirst) {
log('🗑️ Clearing existing categories...');
await txn.delete('categories');
}
log('💾 Batch saving ${categories.length} categories...');
// Batch insert categories
final batch = txn.batch();
for (final category in categories) {
batch.insert(
'categories',
_categoryToMap(category),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
await batch.commit(noResult: true);
});
// Clear cache after update
clearCache();
log('✅ Successfully batch saved ${categories.length} categories');
} catch (e) {
log('❌ Error batch saving categories: $e');
rethrow;
}
}
// ========================================
// CACHED QUERY
// ========================================
Future<List<CategoryModel>> getCachedCategories({
int page = 1,
int limit = 10,
bool isActive = true,
String? search,
}) async {
final cacheKey = _generateCacheKey(page, limit, isActive, search);
final now = DateTime.now();
// Check cache first
if (_queryCache.containsKey(cacheKey) &&
_cacheTimestamps.containsKey(cacheKey)) {
final cacheTime = _cacheTimestamps[cacheKey]!;
if (now.difference(cacheTime) < _cacheExpiry) {
log('🚀 Cache HIT: $cacheKey (${_queryCache[cacheKey]!.length} categories)');
return _queryCache[cacheKey]!;
}
}
log('📀 Cache MISS: $cacheKey, querying database...');
// Cache miss, query database
final categories = await getCategories(
page: page,
limit: limit,
isActive: isActive,
search: search,
);
// Store in cache
_queryCache[cacheKey] = categories;
_cacheTimestamps[cacheKey] = now;
log('💾 Cached ${categories.length} categories for key: $cacheKey');
return categories;
}
// ========================================
// REGULAR GET CATEGORIES
// ========================================
Future<List<CategoryModel>> getCategories({
int page = 1,
int limit = 10,
bool isActive = true,
String? search,
}) async {
final db = await _db;
try {
String query = 'SELECT * FROM categories WHERE 1=1';
List<dynamic> whereArgs = [];
// Note: Assuming is_active will be added to database schema
if (isActive) {
query += ' AND is_active = ?';
whereArgs.add(1);
}
if (search != null && search.isNotEmpty) {
query += ' AND (name LIKE ? OR description LIKE ?)';
whereArgs.add('%$search%');
whereArgs.add('%$search%');
}
// query += ' ORDER BY name ASC';
if (limit > 0) {
query += ' LIMIT ?';
whereArgs.add(limit);
if (page > 1) {
query += ' OFFSET ?';
whereArgs.add((page - 1) * limit);
}
}
final List<Map<String, dynamic>> maps =
await db.rawQuery(query, whereArgs);
List<CategoryModel> categories = [];
for (final map in maps) {
categories.add(_mapToCategory(map));
}
log('📊 Retrieved ${categories.length} categories from database');
return categories;
} catch (e) {
log('❌ Error getting categories: $e');
return [];
}
}
// ========================================
// GET ALL CATEGORIES (For dropdowns)
// ========================================
Future<List<CategoryModel>> getAllCategories() async {
const cacheKey = 'all_categories';
final now = DateTime.now();
// Check cache
if (_queryCache.containsKey(cacheKey) &&
_cacheTimestamps.containsKey(cacheKey)) {
final cacheTime = _cacheTimestamps[cacheKey]!;
if (now.difference(cacheTime) < _cacheExpiry) {
return _queryCache[cacheKey]!;
}
}
final db = await _db;
try {
final List<Map<String, dynamic>> maps = await db.query(
'categories',
orderBy: 'name ASC',
);
final categories = maps.map((map) => _mapToCategory(map)).toList();
// Cache all categories
_queryCache[cacheKey] = categories;
_cacheTimestamps[cacheKey] = now;
log('📊 Retrieved ${categories.length} total categories');
return categories;
} catch (e) {
log('❌ Error getting all categories: $e');
return [];
}
}
// ========================================
// GET CATEGORY BY ID
// ========================================
Future<CategoryModel?> getCategoryById(String id) async {
final db = await _db;
try {
final List<Map<String, dynamic>> maps = await db.query(
'categories',
where: 'id = ?',
whereArgs: [id],
);
if (maps.isEmpty) {
log('❌ Category not found: $id');
return null;
}
final category = _mapToCategory(maps.first);
log('✅ Category found: ${category.name}');
return category;
} catch (e) {
log('❌ Error getting category by ID: $e');
return null;
}
}
// ========================================
// GET TOTAL COUNT
// ========================================
Future<int> getTotalCount({bool isActive = true, String? search}) async {
final db = await _db;
try {
String query = 'SELECT COUNT(*) FROM categories WHERE 1=1';
List<dynamic> whereArgs = [];
if (isActive) {
query += ' AND is_active = ?';
whereArgs.add(1);
}
if (search != null && search.isNotEmpty) {
query += ' AND (name LIKE ? OR description LIKE ?)';
whereArgs.add('%$search%');
whereArgs.add('%$search%');
}
final result = await db.rawQuery(query, whereArgs);
final count = Sqflite.firstIntValue(result) ?? 0;
log('📊 Category total count: $count (isActive: $isActive, search: $search)');
return count;
} catch (e) {
log('❌ Error getting category total count: $e');
return 0;
}
}
// ========================================
// HAS CATEGORIES
// ========================================
Future<bool> hasCategories() async {
final count = await getTotalCount();
final hasData = count > 0;
log('🔍 Has categories: $hasData ($count categories)');
return hasData;
}
// ========================================
// CLEAR ALL CATEGORIES
// ========================================
Future<void> clearAllCategories() async {
final db = await _db;
try {
await db.delete('categories');
clearCache();
log('🗑️ All categories cleared from local DB');
} catch (e) {
log('❌ Error clearing categories: $e');
rethrow;
}
}
// ========================================
// CACHE MANAGEMENT
// ========================================
String _generateCacheKey(int page, int limit, bool isActive, String? search) {
return 'categories_${page}_${limit}_${isActive}_${search ?? 'null'}';
}
void clearCache() {
final count = _queryCache.length;
_queryCache.clear();
_cacheTimestamps.clear();
log('🧹 Category cache cleared: $count entries removed');
}
void clearExpiredCache() {
final now = DateTime.now();
final expiredKeys = <String>[];
_cacheTimestamps.forEach((key, timestamp) {
if (now.difference(timestamp) > _cacheExpiry) {
expiredKeys.add(key);
}
});
for (final key in expiredKeys) {
_queryCache.remove(key);
_cacheTimestamps.remove(key);
}
if (expiredKeys.isNotEmpty) {
log('⏰ Expired category cache cleared: ${expiredKeys.length} entries');
}
}
// ========================================
// DATABASE STATS
// ========================================
Future<Map<String, dynamic>> getDatabaseStats() async {
final db = await _db;
try {
final categoryCount = Sqflite.firstIntValue(
await db.rawQuery('SELECT COUNT(*) FROM categories')) ??
0;
final activeCount = Sqflite.firstIntValue(await db.rawQuery(
'SELECT COUNT(*) FROM categories WHERE is_active = 1')) ??
0;
final stats = {
'total_categories': categoryCount,
'active_categories': activeCount,
'cache_entries': _queryCache.length,
};
log('📊 Category Database Stats: $stats');
return stats;
} catch (e) {
log('❌ Error getting category database stats: $e');
return {};
}
}
// ========================================
// HELPER METHODS
// ========================================
Map<String, dynamic> _categoryToMap(CategoryModel category) {
return {
'id': category.id,
'organization_id': category.organizationId,
'name': category.name,
'description': category.description,
'business_type': category.businessType,
'metadata': json.encode(category.metadata),
'is_active': 1, // Assuming all synced categories are active
'created_at': category.createdAt.toIso8601String(),
'updated_at': category.updatedAt.toIso8601String(),
};
}
CategoryModel _mapToCategory(Map<String, dynamic> map) {
return CategoryModel(
id: map['id'],
organizationId: map['organization_id'],
name: map['name'],
description: map['description'],
businessType: map['business_type'],
metadata: map['metadata'] != null ? json.decode(map['metadata']) : {},
createdAt: DateTime.parse(map['created_at']),
updatedAt: DateTime.parse(map['updated_at']),
);
}
}
@@ -405,7 +405,7 @@ class OrderRemoteDatasource {
} }
} }
Future<Either<String, OrderDetailResponseModel>> addToOrder({ Future<Either<String, bool>> addToOrder({
required String orderId, required String orderId,
required List<OrderItemRequest> orderItems, required List<OrderItemRequest> orderItems,
}) async { }) async {
@@ -429,8 +429,7 @@ class OrderRemoteDatasource {
); );
if (response.statusCode == 200) { if (response.statusCode == 200) {
final data = OrderDetailResponseModel.fromMap(response.data); return Right(true);
return Right(data);
} else { } else {
return const Left('Gagal menambahkan pesanan pesanan'); return const Left('Gagal menambahkan pesanan pesanan');
} }
@@ -442,7 +441,7 @@ class OrderRemoteDatasource {
return Left(errorMessage); return Left(errorMessage);
} catch (e) { } catch (e) {
log("💥 Unexpected error: $e"); log("💥 Unexpected error: $e");
return const Left('Terjadi kesalahan tak terduga'); return const Left('Terjadi kesalahan, coba lagi nanti.');
} }
} }
@@ -2,12 +2,12 @@ import 'dart:developer';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:enaklo_pos/core/network/dio_client.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/outlet/outlet_local_datasource.dart';
import 'package:enaklo_pos/data/datasources/settings_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/home/models/outlet_model.dart';
import 'package:enaklo_pos/presentation/setting/models/tax_model.dart'; import 'package:enaklo_pos/presentation/setting/models/tax_model.dart';
import '../../core/constants/variables.dart'; import '../../../core/constants/variables.dart';
import 'auth_local_datasource.dart'; import '../auth_local_datasource.dart';
class OutletRemoteDataSource { class OutletRemoteDataSource {
final Dio dio = DioClient.instance; final Dio dio = DioClient.instance;
@@ -0,0 +1,317 @@
import 'dart:developer';
import 'package:enaklo_pos/core/database/database_handler.dart';
import 'package:enaklo_pos/data/models/response/print_model.dart';
import 'package:sqflite/sqflite.dart';
class PrinterLocalDatasource {
static PrinterLocalDatasource? _instance;
PrinterLocalDatasource._internal();
static PrinterLocalDatasource get instance {
_instance ??= PrinterLocalDatasource._internal();
return _instance!;
}
Future<Database> get _db async => await DatabaseHelper.instance.database;
// Create new printer
Future<int> createPrinter(PrintModel printer) async {
final db = await _db;
try {
log('Creating printer: ${printer.toString()}');
final id = await db.insert(
'printers',
printer.toMapForInsert(),
conflictAlgorithm:
ConflictAlgorithm.abort, // Fail if code already exists
);
log('Successfully created printer with ID: $id');
return id;
} catch (e) {
log('Error creating printer: $e');
rethrow;
}
}
// Update existing printer
Future<void> updatePrinter(PrintModel printer, int id) async {
final db = await _db;
try {
log('Updating printer ID $id: ${printer.toString()}');
final updatedRows = await db.update(
'printers',
printer.toMapForUpdate(),
where: 'id = ?',
whereArgs: [id],
);
if (updatedRows == 0) {
throw Exception('Printer with ID $id not found');
}
log('Successfully updated printer ID: $id');
} catch (e) {
log('Error updating printer: $e');
rethrow;
}
}
// Delete printer by ID
Future<void> deletePrinter(int id) async {
final db = await _db;
try {
log('Deleting printer ID: $id');
final deletedRows = await db.delete(
'printers',
where: 'id = ?',
whereArgs: [id],
);
if (deletedRows == 0) {
throw Exception('Printer with ID $id not found');
}
log('Successfully deleted printer ID: $id');
} catch (e) {
log('Error deleting printer: $e');
rethrow;
}
}
// Get printer by code
Future<PrintModel?> getPrinterByCode(String code) async {
final db = await _db;
try {
log('Getting printer by code: $code');
final result = await db.query(
'printers',
where: 'code = ?',
whereArgs: [code],
);
if (result.isEmpty) {
log('Printer with code $code not found');
return null;
}
final printer = PrintModel.fromMap(result.first);
log('Found printer: ${printer.toString()}');
return printer;
} catch (e) {
log('Error getting printer by code: $e');
return null;
}
}
// Get printer by ID
Future<PrintModel?> getPrinterById(int id) async {
final db = await _db;
try {
log('Getting printer by ID: $id');
final result = await db.query(
'printers',
where: 'id = ?',
whereArgs: [id],
);
if (result.isEmpty) {
log('Printer with ID $id not found');
return null;
}
final printer = PrintModel.fromMap(result.first);
log('Found printer: ${printer.toString()}');
return printer;
} catch (e) {
log('Error getting printer by ID: $e');
return null;
}
}
// Get all printers
Future<List<PrintModel>> getAllPrinters() async {
final db = await _db;
try {
log('Getting all printers');
final result = await db.query(
'printers',
orderBy: 'name ASC',
);
final printers = result.map((map) => PrintModel.fromMap(map)).toList();
log('Found ${printers.length} printers');
return printers;
} catch (e) {
log('Error getting all printers: $e');
return [];
}
}
// Get printers by type
Future<List<PrintModel>> getPrintersByType(String type) async {
final db = await _db;
try {
log('Getting printers by type: $type');
final result = await db.query(
'printers',
where: 'type = ?',
whereArgs: [type],
orderBy: 'name ASC',
);
final printers = result.map((map) => PrintModel.fromMap(map)).toList();
log('Found ${printers.length} printers with type $type');
return printers;
} catch (e) {
log('Error getting printers by type: $e');
return [];
}
}
// Search printers by name
Future<List<PrintModel>> searchPrintersByName(String query) async {
final db = await _db;
try {
log('Searching printers by name: $query');
final result = await db.query(
'printers',
where: 'name LIKE ?',
whereArgs: ['%$query%'],
orderBy: 'name ASC',
);
final printers = result.map((map) => PrintModel.fromMap(map)).toList();
log('Found ${printers.length} printers matching "$query"');
return printers;
} catch (e) {
log('Error searching printers: $e');
return [];
}
}
// Check if printer code exists
Future<bool> isPrinterCodeExists(String code, {int? excludeId}) async {
final db = await _db;
try {
String whereClause = 'code = ?';
List<dynamic> whereArgs = [code];
if (excludeId != null) {
whereClause += ' AND id != ?';
whereArgs.add(excludeId);
}
final result = await db.query(
'printers',
where: whereClause,
whereArgs: whereArgs,
);
final exists = result.isNotEmpty;
log('Printer code "$code" exists: $exists');
return exists;
} catch (e) {
log('Error checking printer code existence: $e');
return false;
}
}
// Get printer statistics
Future<Map<String, dynamic>> getPrinterStats() async {
final db = await _db;
try {
// Total count
final totalResult =
await db.rawQuery('SELECT COUNT(*) as total FROM printers');
final totalCount = totalResult.first['total'] as int;
// Count by type
final typeResult = await db.rawQuery('''
SELECT type, COUNT(*) as count
FROM printers
WHERE type IS NOT NULL
GROUP BY type
''');
final typeStats = <String, int>{};
for (final row in typeResult) {
typeStats[row['type'] as String] = row['count'] as int;
}
final stats = {
'total_printers': totalCount,
'by_type': typeStats,
};
log('Printer stats: $stats');
return stats;
} catch (e) {
log('Error getting printer stats: $e');
return {
'total_printers': 0,
'by_type': <String, int>{},
};
}
}
// Clear all printers (for testing/reset purposes)
Future<void> clearAllPrinters() async {
final db = await _db;
try {
log('Clearing all printers');
await db.delete('printers');
log('All printers cleared');
} catch (e) {
log('Error clearing printers: $e');
rethrow;
}
}
// Batch insert printers
Future<void> insertPrinters(List<PrintModel> printers) async {
final db = await _db;
try {
log('Batch inserting ${printers.length} printers');
await db.transaction((txn) async {
for (final printer in printers) {
await txn.insert(
'printers',
printer.toMapForInsert(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
});
log('Successfully batch inserted ${printers.length} printers');
} catch (e) {
log('Error batch inserting printers: $e');
rethrow;
}
}
}
@@ -0,0 +1,496 @@
import 'dart:convert';
import 'dart:developer';
import 'dart:io';
import 'package:enaklo_pos/core/database/database_handler.dart';
import 'package:enaklo_pos/data/models/response/product_response_model.dart';
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart' as p;
class ProductLocalDatasource {
static ProductLocalDatasource? _instance;
ProductLocalDatasource._internal();
static ProductLocalDatasource get instance {
_instance ??= ProductLocalDatasource._internal();
return _instance!;
}
Future<Database> get _db async => await DatabaseHelper.instance.database;
// ========================================
// CACHING SYSTEM
// ========================================
final Map<String, List<Product>> _queryCache = {};
final Duration _cacheExpiry = Duration(minutes: 5);
final Map<String, DateTime> _cacheTimestamps = {};
// ========================================
// ENHANCED BATCH SAVE
// ========================================
Future<void> saveProductsBatch(List<Product> products,
{bool clearFirst = false}) async {
final db = await _db;
try {
await db.transaction((txn) async {
if (clearFirst) {
log('🗑️ Clearing existing products...');
await txn.delete('product_variants');
await txn.delete('products');
}
log('💾 Batch saving ${products.length} products...');
// ✅ BATCH INSERT PRODUCTS - Much faster than individual inserts
final batch = txn.batch();
for (final product in products) {
batch.insert(
'products',
_productToMap(product),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
await batch.commit(noResult: true);
// ✅ BATCH INSERT VARIANTS
final variantBatch = txn.batch();
for (final product in products) {
if (product.variants?.isNotEmpty == true) {
// Delete existing variants in batch
variantBatch.delete(
'product_variants',
where: 'product_id = ?',
whereArgs: [product.id],
);
// Insert new variants
for (final variant in product.variants!) {
variantBatch.insert(
'product_variants',
_variantToMap(variant),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
}
}
await variantBatch.commit(noResult: true);
});
// Clear cache after update
clearCache();
log('✅ Successfully batch saved ${products.length} products');
} catch (e) {
log('❌ Error batch saving products: $e');
rethrow;
}
}
// ========================================
// CACHED QUERY - HIGH PERFORMANCE
// ========================================
Future<List<Product>> getCachedProducts({
int page = 1,
int limit = 10,
String? categoryId,
String? search,
}) async {
final cacheKey = _generateCacheKey(page, limit, categoryId, search);
final now = DateTime.now();
// ✅ CHECK CACHE FIRST
if (_queryCache.containsKey(cacheKey) &&
_cacheTimestamps.containsKey(cacheKey)) {
final cacheTime = _cacheTimestamps[cacheKey]!;
if (now.difference(cacheTime) < _cacheExpiry) {
log('🚀 Cache HIT: $cacheKey (${_queryCache[cacheKey]!.length} products)');
return _queryCache[cacheKey]!; // Return from cache - SUPER FAST
}
}
log('📀 Cache MISS: $cacheKey, querying database...');
// Cache miss, query database
final products = await getProducts(
page: page,
limit: limit,
categoryId: categoryId,
search: search,
);
// ✅ STORE IN CACHE for next time
_queryCache[cacheKey] = products;
_cacheTimestamps[cacheKey] = now;
log('💾 Cached ${products.length} products for key: $cacheKey');
return products;
}
// ========================================
// REGULAR GET PRODUCTS (No Cache)
// ========================================
Future<List<Product>> getProducts({
int page = 1,
int limit = 10,
String? categoryId,
String? search,
}) async {
final db = await _db;
try {
String query = 'SELECT * FROM products WHERE 1=1';
List<dynamic> whereArgs = [];
if (categoryId != null && categoryId.isNotEmpty) {
query += ' AND category_id = ?';
whereArgs.add(categoryId);
}
if (search != null && search.isNotEmpty) {
query += ' AND (name LIKE ? OR sku LIKE ? OR description LIKE ?)';
whereArgs.add('%$search%');
whereArgs.add('%$search%');
whereArgs.add('%$search%');
}
query += ' ORDER BY created_at DESC';
if (limit > 0) {
query += ' LIMIT ?';
whereArgs.add(limit);
if (page > 1) {
query += ' OFFSET ?';
whereArgs.add((page - 1) * limit);
}
}
final List<Map<String, dynamic>> maps =
await db.rawQuery(query, whereArgs);
List<Product> products = [];
for (final map in maps) {
final variants = await _getProductVariants(db, map['id']);
final product = _mapToProduct(map, variants);
products.add(product);
}
log('📊 Retrieved ${products.length} products from database');
return products;
} catch (e) {
log('❌ Error getting products: $e');
return [];
}
}
// ========================================
// OPTIMIZED SEARCH with RANKING
// ========================================
Future<List<Product>> searchProductsOptimized(String query) async {
final db = await _db;
try {
log('🔍 Optimized search for: "$query"');
// ✅ Smart query with prioritization
final List<Map<String, dynamic>> maps = await db.rawQuery('''
SELECT * FROM products
WHERE name LIKE ? OR sku LIKE ? OR description LIKE ?
ORDER BY
CASE
WHEN name LIKE ? THEN 1 -- Highest priority: name match
WHEN sku LIKE ? THEN 2 -- Second priority: SKU match
ELSE 3 -- Lowest priority: description
END,
name ASC
LIMIT 50
''', [
'%$query%', '%$query%', '%$query%',
'$query%', '$query%' // Prioritize results that start with query
]);
List<Product> products = [];
for (final map in maps) {
final variants = await _getProductVariants(db, map['id']);
products.add(_mapToProduct(map, variants));
}
log('🎯 Optimized search found ${products.length} results');
return products;
} catch (e) {
log('❌ Error in optimized search: $e');
return [];
}
}
// ========================================
// DATABASE ANALYTICS & MONITORING
// ========================================
Future<Map<String, dynamic>> getDatabaseStats() async {
final db = await _db;
try {
final productCount = Sqflite.firstIntValue(
await db.rawQuery('SELECT COUNT(*) FROM products')) ??
0;
final variantCount = Sqflite.firstIntValue(
await db.rawQuery('SELECT COUNT(*) FROM product_variants')) ??
0;
final categoryCount = Sqflite.firstIntValue(await db.rawQuery(
'SELECT COUNT(DISTINCT category_id) FROM products WHERE category_id IS NOT NULL')) ??
0;
final dbSize = await _getDatabaseSize();
final stats = {
'total_products': productCount,
'total_variants': variantCount,
'total_categories': categoryCount,
'database_size_mb': dbSize,
'cache_entries': _queryCache.length,
'cache_size_mb': _getCacheSize(),
};
log('📊 Database Stats: $stats');
return stats;
} catch (e) {
log('❌ Error getting database stats: $e');
return {};
}
}
Future<double> _getDatabaseSize() async {
try {
final dbPath = p.join(await getDatabasesPath(), 'pos_database.db');
final file = File(dbPath);
if (await file.exists()) {
final size = await file.length();
return size / (1024 * 1024); // Convert to MB
}
} catch (e) {
log('Error getting database size: $e');
}
return 0.0;
}
double _getCacheSize() {
double totalSize = 0;
_queryCache.forEach((key, products) {
totalSize += products.length * 0.001; // Rough estimate in MB
});
return totalSize;
}
// ========================================
// CACHE MANAGEMENT
// ========================================
String _generateCacheKey(
int page, int limit, String? categoryId, String? search) {
return 'products_${page}_${limit}_${categoryId ?? 'null'}_${search ?? 'null'}';
}
void clearCache() {
final count = _queryCache.length;
_queryCache.clear();
_cacheTimestamps.clear();
log('🧹 Cache cleared: $count entries removed');
}
void clearExpiredCache() {
final now = DateTime.now();
final expiredKeys = <String>[];
_cacheTimestamps.forEach((key, timestamp) {
if (now.difference(timestamp) > _cacheExpiry) {
expiredKeys.add(key);
}
});
for (final key in expiredKeys) {
_queryCache.remove(key);
_cacheTimestamps.remove(key);
}
if (expiredKeys.isNotEmpty) {
log('⏰ Expired cache cleared: ${expiredKeys.length} entries');
}
}
// ========================================
// OTHER METHODS (Same as basic but with enhanced logging)
// ========================================
Future<Product?> getProductById(String id) async {
final db = await _db;
try {
final List<Map<String, dynamic>> maps = await db.query(
'products',
where: 'id = ?',
whereArgs: [id],
);
if (maps.isEmpty) {
log('❌ Product not found: $id');
return null;
}
final variants = await _getProductVariants(db, id);
final product = _mapToProduct(maps.first, variants);
log('✅ Product found: ${product.name}');
return product;
} catch (e) {
log('❌ Error getting product by ID: $e');
return null;
}
}
Future<int> getTotalCount({String? categoryId, String? search}) async {
final db = await _db;
try {
String query = 'SELECT COUNT(*) FROM products WHERE 1=1';
List<dynamic> whereArgs = [];
if (categoryId != null && categoryId.isNotEmpty) {
query += ' AND category_id = ?';
whereArgs.add(categoryId);
}
if (search != null && search.isNotEmpty) {
query += ' AND (name LIKE ? OR sku LIKE ? OR description LIKE ?)';
whereArgs.add('%$search%');
whereArgs.add('%$search%');
whereArgs.add('%$search%');
}
final result = await db.rawQuery(query, whereArgs);
final count = Sqflite.firstIntValue(result) ?? 0;
log('📊 Total count: $count (categoryId: $categoryId, search: $search)');
return count;
} catch (e) {
log('❌ Error getting total count: $e');
return 0;
}
}
Future<bool> hasProducts() async {
final count = await getTotalCount();
final hasData = count > 0;
log('🔍 Has products: $hasData ($count products)');
return hasData;
}
Future<void> clearAllProducts() async {
final db = await _db;
try {
await db.transaction((txn) async {
await txn.delete('product_variants');
await txn.delete('products');
});
clearCache();
log('🗑️ All products cleared from local DB');
} catch (e) {
log('❌ Error clearing products: $e');
rethrow;
}
}
// ========================================
// HELPER METHODS
// ========================================
Future<List<ProductVariant>> _getProductVariants(
Database db, String productId) async {
try {
final List<Map<String, dynamic>> maps = await db.query(
'product_variants',
where: 'product_id = ?',
whereArgs: [productId],
orderBy: 'name ASC',
);
return maps.map((map) => _mapToVariant(map)).toList();
} catch (e) {
log('❌ Error getting variants for product $productId: $e');
return [];
}
}
Map<String, dynamic> _productToMap(Product product) {
return {
'id': product.id,
'organization_id': product.organizationId,
'category_id': product.categoryId,
'sku': product.sku,
'name': product.name,
'description': product.description,
'price': product.price,
'cost': product.cost,
'business_type': product.businessType,
'image_url': product.imageUrl,
'printer_type': product.printerType,
'metadata':
product.metadata != null ? json.encode(product.metadata) : null,
'is_active': product.isActive == true ? 1 : 0,
'created_at': product.createdAt?.toIso8601String(),
'updated_at': product.updatedAt?.toIso8601String(),
};
}
Map<String, dynamic> _variantToMap(ProductVariant variant) {
return {
'id': variant.id,
'product_id': variant.productId,
'name': variant.name,
'price_modifier': variant.priceModifier,
'cost': variant.cost,
'metadata':
variant.metadata != null ? json.encode(variant.metadata) : null,
'created_at': variant.createdAt?.toIso8601String(),
'updated_at': variant.updatedAt?.toIso8601String(),
};
}
Product _mapToProduct(
Map<String, dynamic> map, List<ProductVariant> variants) {
return Product(
id: map['id'],
organizationId: map['organization_id'],
categoryId: map['category_id'],
sku: map['sku'],
name: map['name'],
description: map['description'],
price: map['price'],
cost: map['cost'],
businessType: map['business_type'],
imageUrl: map['image_url'],
printerType: map['printer_type'],
metadata: map['metadata'] != null ? json.decode(map['metadata']) : null,
isActive: map['is_active'] == 1,
createdAt:
map['created_at'] != null ? DateTime.parse(map['created_at']) : null,
updatedAt:
map['updated_at'] != null ? DateTime.parse(map['updated_at']) : null,
variants: variants,
);
}
ProductVariant _mapToVariant(Map<String, dynamic> map) {
return ProductVariant(
id: map['id'],
productId: map['product_id'],
name: map['name'],
priceModifier: map['price_modifier'],
cost: map['cost'],
metadata: map['metadata'] != null ? json.decode(map['metadata']) : null,
createdAt:
map['created_at'] != null ? DateTime.parse(map['created_at']) : null,
updatedAt:
map['updated_at'] != null ? DateTime.parse(map['updated_at']) : null,
);
}
}
@@ -1,639 +0,0 @@
import 'dart:developer';
import 'dart:ui';
import 'package:enaklo_pos/data/models/response/print_model.dart';
import 'package:enaklo_pos/data/models/response/product_response_model.dart';
import 'package:enaklo_pos/data/models/response/table_model.dart';
import 'package:enaklo_pos/presentation/home/models/order_model.dart';
import 'package:enaklo_pos/presentation/table/models/draft_order_item.dart';
import 'package:enaklo_pos/presentation/table/models/draft_order_model.dart';
import 'package:sqflite/sqflite.dart';
import '../../presentation/home/models/product_quantity.dart';
class ProductLocalDatasource {
ProductLocalDatasource._init();
static final ProductLocalDatasource instance = ProductLocalDatasource._init();
final String tableProduct = 'products';
final String tableOrder = 'orders';
final String tableOrderItem = 'order_items';
final String tableManagement = 'table_management';
final String tablePrint = 'prints';
static Database? _database;
// "id": 1,
// "category_id": 1,
// "name": "Mie Ayam",
// "description": "Ipsa dolorem impedit dolor. Libero nisi quidem expedita quod mollitia ad. Voluptas ut quia nemo nisi odit fuga. Fugit autem qui ratione laborum eum.",
// "image": "https://via.placeholder.com/640x480.png/002200?text=nihil",
// "price": "2000.44",
// "stock": 94,
// "status": 1,
// "is_favorite": 1,
// "created_at": "2024-02-08T14:30:22.000000Z",
// "updated_at": "2024-02-08T15:14:22.000000Z"
Future<void> _createDb(Database db, int version) async {
await db.execute('''
CREATE TABLE $tableProduct (
id INTEGER PRIMARY KEY,
product_id INTEGER,
name TEXT,
printer_type TEXT,
categoryId INTEGER,
categoryName TEXT,
description TEXT,
image TEXT,
price TEXT,
stock INTEGER,
status INTEGER,
isFavorite INTEGER,
createdAt TEXT,
updatedAt TEXT
)
''');
await db.execute('''
CREATE TABLE $tableOrder (
id INTEGER PRIMARY KEY AUTOINCREMENT,
payment_amount INTEGER,
sub_total INTEGER,
tax INTEGER,
discount INTEGER,
discount_amount INTEGER,
service_charge INTEGER,
total INTEGER,
payment_method TEXT,
total_item INTEGER,
id_kasir INTEGER,
nama_kasir TEXT,
transaction_time TEXT,
table_number INTEGER,
customer_name TEXT,
status TEXT,
payment_status TEXT,
order_type TEXT DEFAULT 'DINE IN',
is_sync INTEGER DEFAULT 0
)
''');
await db.execute('''
CREATE TABLE $tableOrderItem (
id INTEGER PRIMARY KEY AUTOINCREMENT,
id_order INTEGER,
id_product INTEGER,
quantity INTEGER,
price INTEGER,
notes TEXT DEFAULT ''
)
''');
await db.execute('''
CREATE TABLE $tableManagement (
id INTEGER PRIMARY KEY AUTOINCREMENT,
table_name Text,
start_time Text,
order_id INTEGER,
payment_amount INTEGER,
x_position REAL NOT NULL,
y_position REAL NOT NULL,
status TEXT
)
''');
await db.execute('''
CREATE TABLE draft_orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
total_item INTEGER,
subtotal INTEGER,
tax INTEGER,
discount INTEGER,
discount_amount INTEGER,
service_charge INTEGER,
total INTEGER,
transaction_time TEXT,
table_number INTEGER,
draft_name TEXT
)
''');
await db.execute('''
CREATE TABLE draft_order_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
id_draft_order INTEGER,
id_product INTEGER,
quantity INTEGER,
price INTEGER
)
''');
await db.execute('''
CREATE TABLE $tablePrint (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT,
name TEXT,
address TEXT,
paper TEXT,
type TEXT
)
''');
}
Future<Database> _initDB(String filePath) async {
final dbPath = await getDatabasesPath();
final path = dbPath + filePath;
// Force delete existing database to ensure new schema
try {
final dbExists = await databaseExists(path);
if (dbExists) {
log("Deleting existing database to ensure new schema with order_type column");
// await deleteDatabase(path);
}
} catch (e) {
log("Error deleting database: $e");
}
return await openDatabase(
path,
version: 2,
onCreate: _createDb,
onUpgrade: _onUpgrade,
);
}
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
if (oldVersion < 2) {
// Add order_type column to orders table if it doesn't exist
try {
await db.execute(
'ALTER TABLE $tableOrder ADD COLUMN order_type TEXT DEFAULT "DINE IN"');
log("Added order_type column to orders table");
} catch (e) {
log("order_type column might already exist: $e");
}
}
}
Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDB('dbresto36.db');
return _database!;
}
//save order
Future<int> saveOrder(OrderModel order) async {
final db = await instance.database;
// Since we're forcing database recreation, order_type column should exist
final orderMap = order.toMap(includeOrderType: true);
log("Final orderMap for insertion: $orderMap");
int id = await db.insert(tableOrder, orderMap,
conflictAlgorithm: ConflictAlgorithm.replace);
for (var item in order.orderItems) {
log("Item: ${item.toLocalMap(id)}");
await db.insert(tableOrderItem, item.toLocalMap(id),
conflictAlgorithm: ConflictAlgorithm.replace);
}
log("Success Order: ${order.toMap()}");
return id;
}
//get data order
Future<List<OrderModel>> getOrderByIsNotSync() async {
final db = await instance.database;
final List<Map<String, dynamic>> maps =
await db.query(tableOrder, where: 'is_sync = ?', whereArgs: [0]);
return List.generate(maps.length, (i) {
return OrderModel.fromMap(maps[i]);
});
}
Future<List<OrderModel>> getAllOrder(
DateTime date,
) async {
final db = await instance.database;
//date to iso8601
final dateIso = date.toIso8601String();
//get yyyy-MM-dd
final dateYYYYMMDD = dateIso.substring(0, 10);
// final formattedDate = DateFormat('yyyy-MM-dd').format(date);
final List<Map<String, dynamic>> maps = await db.query(
tableOrder,
where: 'transaction_time like ?',
whereArgs: ['$dateYYYYMMDD%'],
// where: 'transaction_time BETWEEN ? AND ?',
// whereArgs: [
// DateFormat.yMd().format(start),
// DateFormat.yMd().format(end)
// ],
);
return List.generate(maps.length, (i) {
log("Save save OrderModel: ${OrderModel.fromMap(maps[i])}");
return OrderModel.fromMap(maps[i]);
});
}
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
Future<List<ProductQuantity>> getOrderItemByOrderId(int orderId) async {
final db = await instance.database;
final List<Map<String, dynamic>> maps = await db
.query(tableOrderItem, where: 'id_order = ?', whereArgs: [orderId]);
return List.generate(maps.length, (i) {
log("ProductQuantity: ${ProductQuantity.fromLocalMap(maps[i])}");
return ProductQuantity.fromLocalMap(maps[i]);
});
}
//update payment status by order id
Future<void> updatePaymentStatus(
int orderId, String paymentStatus, String status) async {
final db = await instance.database;
await db.update(
tableOrder, {'payment_status': paymentStatus, 'status': status},
where: 'id = ?', whereArgs: [orderId]);
log('update payment status success | order id: $orderId | payment status: $paymentStatus | status: $status');
}
//update order is sync
Future<void> updateOrderIsSync(int orderId) async {
final db = await instance.database;
await db.update(tableOrder, {'is_sync': 1},
where: 'id = ?', whereArgs: [orderId]);
}
//insert data product
Future<void> insertProduct(Product product) async {
log("Product: ${product.toMap()}");
final db = await instance.database;
await db.insert(tableProduct, product.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace);
}
//update product
Future<void> updateProduct(Product product) async {
log("Update Product: ${product.toMap()}");
final db = await instance.database;
await db.update(
tableProduct,
product.toLocalMap(),
where: 'product_id = ?',
whereArgs: [product.id],
);
}
//insert list of product
Future<void> insertProducts(List<Product> products) async {
final db = await instance.database;
log("Save Products to Local");
for (var product in products) {
await db.insert(tableProduct, product.toLocalMap(),
conflictAlgorithm: ConflictAlgorithm.replace);
log('inserted success id: ${product.id} | name: ${product.name} | price: ${product.price} ');
}
}
//get all products
Future<List<Product>> getProducts() async {
final db = await instance.database;
final List<Map<String, dynamic>> maps = await db.query(tableProduct);
return List.generate(maps.length, (i) {
return Product.fromLocalMap(maps[i]);
});
}
Future<Product?> getProductById(int id) async {
final db = await instance.database;
final result =
await db.query(tableProduct, where: 'product_id = ?', whereArgs: [id]);
if (result.isEmpty) {
return null;
}
return Product.fromMap(result.first);
}
// get Last Table Management
Future<TableModel?> getLastTableManagement() async {
final db = await instance.database;
final List<Map<String, dynamic>> maps =
await db.query(tableManagement, orderBy: 'id DESC', limit: 1);
if (maps.isEmpty) {
return null;
}
return TableModel.fromMap(maps[0]);
}
// generate table managent with count
Future<void> createTableManagement(String tableName, Offset position) async {
// final db = await instance.database;
// TableModel newTable = TableModel(
// tableName: tableName,
// status: 'available',
// orderId: 0,
// paymentAmount: 0,
// startTime: DateTime.now().toIso8601String(),
// position: position,
// );
// await db.insert(
// tableManagement,
// newTable.toMap(),
// );
}
// change position table
Future<void> changePositionTable(int id, Offset position) async {
final db = await instance.database;
await db.update(
tableManagement,
{'x_position': position.dx, 'y_position': position.dy},
where: 'id = ?',
whereArgs: [id],
);
}
// update table
Future<void> updateTable(TableModel table) async {
final db = await instance.database;
await db.update(
tableManagement,
table.toMap(),
where: 'id = ?',
whereArgs: [table.id],
);
}
// get all table
Future<List<TableModel>> getAllTable() async {
final db = await instance.database;
final List<Map<String, dynamic>> maps = await db.query(tableManagement);
log("Table Management: $maps");
return List.generate(maps.length, (i) {
return TableModel.fromMap(maps[i]);
});
}
// get last order where table number
Future<OrderModel?> getLastOrderTable(int tableNumber) async {
final db = await instance.database;
final List<Map<String, dynamic>> maps = await db.query(
tableOrder,
where: 'table_number = ?',
whereArgs: [tableNumber],
orderBy: 'id DESC', // Urutkan berdasarkan id dari yang terbesar (terbaru)
limit: 1, // Ambil hanya satu data terakhir
);
if (maps.isEmpty) {
return null;
}
return OrderModel.fromMap(maps[0]);
}
// get table by status
Future<List<TableModel>> getTableByStatus(String status) async {
final db = await instance.database;
List<Map<String, dynamic>> maps;
if (status == 'all') {
// Get all tables
maps = await db.query(tableManagement);
log("Getting all tables, found: ${maps.length}");
// If no tables exist, create some default tables
if (maps.isEmpty) {
log("No tables found, creating default tables...");
await _createDefaultTables();
maps = await db.query(tableManagement);
log("After creating default tables, found: ${maps.length}");
}
} else {
// Get tables by specific status
maps = await db.query(
tableManagement,
where: 'status = ?',
whereArgs: [status],
);
log("Getting tables with status '$status', found: ${maps.length}");
}
final tables = List.generate(maps.length, (i) {
return TableModel.fromMap(maps[i]);
});
log("Returning ${tables.length} tables");
tables.forEach((table) {
log("Table: ${table.tableName} (ID: ${table.id}, Status: ${table.status})");
});
return tables;
}
// Create default tables if none exist
Future<void> _createDefaultTables() async {
final db = await instance.database;
// Create 5 default tables
for (int i = 1; i <= 5; i++) {
await db.insert(tableManagement, {
'table_name': 'Table $i',
'start_time': DateTime.now().toIso8601String(),
'order_id': 0,
'payment_amount': 0,
'x_position': 100.0 + (i * 50.0),
'y_position': 100.0 + (i * 50.0),
'status': 'available',
});
log("Created default table: Table $i");
}
}
// update status tabel
Future<void> updateStatusTable(TableModel table) async {
log("Updating table status: ${table.toMap()}");
final db = await instance.database;
await db.update(tableManagement, table.toMap(),
where: 'id = ?', whereArgs: [table.id]);
log("Success Update Status Table: ${table.toMap()}");
// Verify the update
final updatedTable = await db.query(
tableManagement,
where: 'id = ?',
whereArgs: [table.id],
);
if (updatedTable.isNotEmpty) {
log("Verified table update: ${updatedTable.first}");
}
}
// Debug method to reset all tables to available status
Future<void> resetAllTablesToAvailable() async {
log("Resetting all tables to available status...");
final db = await instance.database;
await db.update(
tableManagement,
{
'status': 'available',
'order_id': 0,
'payment_amount': 0,
'start_time': DateTime.now().toIso8601String(),
},
);
log("All tables reset to available status");
}
//delete all products
Future<void> deleteAllProducts() async {
final db = await instance.database;
await db.delete(tableProduct);
}
Future<int> saveDraftOrder(DraftOrderModel order) async {
log("save draft order: ${order.toMapForLocal()}");
final db = await instance.database;
int id = await db.insert('draft_orders', order.toMapForLocal());
log("draft order id: $id | ${order.discountAmount}");
for (var orderItem in order.orders) {
await db.insert('draft_order_items', orderItem.toMapForLocal(id));
log("draft order item ${orderItem.toMapForLocal(id)}");
}
return id;
}
//get all draft order
Future<List<DraftOrderModel>> getAllDraftOrder() async {
final db = await instance.database;
final result = await db.query('draft_orders', orderBy: 'id ASC');
List<DraftOrderModel> results = await Future.wait(result.map((item) async {
// Your asynchronous operation here
final draftOrderItem =
await getDraftOrderItemByOrderId(item['id'] as int);
return DraftOrderModel.newFromLocalMap(item, draftOrderItem);
}));
return results;
}
// get Darft Order by id
Future<DraftOrderModel?> getDraftOrderById(int id) async {
final db = await instance.database;
final result =
await db.query('draft_orders', where: 'id = ?', whereArgs: [id]);
if (result.isEmpty) {
return null;
}
final draftOrderItem =
await getDraftOrderItemByOrderId(result.first['id'] as int);
log("draft order item: $draftOrderItem | ${result.first.toString()}");
return DraftOrderModel.newFromLocalMap(result.first, draftOrderItem);
}
//get draft order item by id order
Future<List<DraftOrderItem>> getDraftOrderItemByOrderId(int idOrder) async {
final db = await instance.database;
final result =
await db.query('draft_order_items', where: 'id_draft_order = $idOrder');
List<DraftOrderItem> results = await Future.wait(result.map((item) async {
// Your asynchronous operation here
final product = await getProductById(item['id_product'] as int);
return DraftOrderItem(
product: product!, quantity: item['quantity'] as int);
}));
return results;
}
//remove draft order by id
Future<void> removeDraftOrderById(int id) async {
final db = await instance.database;
await db.delete('draft_orders', where: 'id = ?', whereArgs: [id]);
await db.delete('draft_order_items',
where: 'id_draft_order = ?', whereArgs: [id]);
}
//update draft order
Future<void> updateDraftOrder(DraftOrderModel draftOrder) async {
final db = await instance.database;
// Update the draft order
await db.update(
'draft_orders',
draftOrder.toMapForLocal(),
where: 'id = ?',
whereArgs: [draftOrder.id],
);
// Remove existing items and add new ones
await db.delete('draft_order_items',
where: 'id_draft_order = ?', whereArgs: [draftOrder.id]);
for (var orderItem in draftOrder.orders) {
await db.insert(
'draft_order_items', orderItem.toMapForLocal(draftOrder.id!));
}
}
/// create printer
Future<void> createPrinter(PrintModel print) async {
final db = await instance.database;
await db.insert(tablePrint, print.toMap());
}
Future<void> updatePrinter(PrintModel print, int id) async {
final db = await instance.database;
log("Update Printer: ${print.toMap()} | id: $id");
await db
.update(tablePrint, print.toMap(), where: 'id = ?', whereArgs: [id]);
}
Future<void> deletePrinter(int id) async {
final db = await instance.database;
await db.delete(tablePrint, where: 'id = ?', whereArgs: [id]);
}
// get printer by code
Future<PrintModel?> getPrinterByCode(String code) async {
final db = await instance.database;
final result =
await db.query(tablePrint, where: 'code = ?', whereArgs: [code]);
if (result.isEmpty) {
return null;
}
return PrintModel.fromMap(result.first);
}
}
@@ -56,7 +56,7 @@ class ProductRemoteDatasource {
log("Dio error: ${e.message}"); log("Dio error: ${e.message}");
return Left(e.response?.data['message'] ?? 'Gagal mengambil produk'); return Left(e.response?.data['message'] ?? 'Gagal mengambil produk');
} catch (e) { } catch (e) {
log("Unexpected error: $e"); log("Unexpected error: $e", name: "productRemoteDatasource");
return const Left('Unexpected error occurred'); return const Left('Unexpected error occurred');
} }
} }
@@ -19,7 +19,7 @@ class SettingsLocalDatasource {
return TaxModel( return TaxModel(
name: 'Tax', name: 'Tax',
type: TaxType.pajak, type: TaxType.pajak,
value: 11, value: 0,
); );
} }
} }
@@ -55,7 +55,7 @@ class TableRemoteDataSource {
Future<Either<String, TableResponseModel>> getTable({ Future<Either<String, TableResponseModel>> getTable({
int page = 1, int page = 1,
int limit = 10, int limit = 50,
String? status, String? status,
}) async { }) async {
try { try {
@@ -131,4 +131,40 @@ class TableRemoteDataSource {
return const Left('Unexpected error occurred'); 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');
}
}
} }
@@ -2,9 +2,11 @@ import 'dart:convert';
class AuthResponseModel { class AuthResponseModel {
final String? token; final String? token;
final String? refreshToken;
final User? user; final User? user;
AuthResponseModel({ AuthResponseModel({
this.refreshToken,
this.token, this.token,
this.user, this.user,
}); });
@@ -17,11 +19,13 @@ class AuthResponseModel {
factory AuthResponseModel.fromMap(Map<String, dynamic> json) => factory AuthResponseModel.fromMap(Map<String, dynamic> json) =>
AuthResponseModel( AuthResponseModel(
token: json["token"], token: json["token"],
refreshToken: json["refresh_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() => {
"token": token, "token": token,
"refresh_token": refreshToken,
"user": user?.toMap(), "user": user?.toMap(),
}; };
} }
@@ -0,0 +1,129 @@
class CategoryAnalyticResponseModel {
final bool success;
final CategoryAnalyticData? data;
final dynamic errors;
CategoryAnalyticResponseModel({
required this.success,
required this.data,
this.errors,
});
// Dari JSON String ke Model
factory CategoryAnalyticResponseModel.fromJson(Map<String, dynamic> json) {
return CategoryAnalyticResponseModel(
success: json['success'],
data: json['data'] == null
? null
: CategoryAnalyticData.fromMap(json['data']),
errors: json['errors'],
);
}
// Dari Model ke JSON String
Map<String, dynamic> toJson() {
return {
'success': success,
'data': data?.toMap(),
'errors': errors,
};
}
// Dari Map ke Model
factory CategoryAnalyticResponseModel.fromMap(Map<String, dynamic> map) {
return CategoryAnalyticResponseModel(
success: map['success'],
data: CategoryAnalyticData.fromMap(map['data']),
errors: map['errors'],
);
}
// Dari Model ke Map
Map<String, dynamic> toMap() {
return {
'success': success,
'data': data?.toMap(),
'errors': errors,
};
}
}
class CategoryAnalyticData {
final String organizationId;
final String outletId;
final DateTime dateFrom;
final DateTime dateTo;
final List<CategoryAnalyticItem> data;
CategoryAnalyticData({
required this.organizationId,
required this.outletId,
required this.dateFrom,
required this.dateTo,
required this.data,
});
factory CategoryAnalyticData.fromMap(Map<String, dynamic> map) {
return CategoryAnalyticData(
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<CategoryAnalyticItem>.from(
map['data']?.map((x) => CategoryAnalyticItem.fromMap(x)) ?? [],
),
);
}
Map<String, dynamic> toMap() {
return {
'organization_id': organizationId,
'outlet_id': outletId,
'date_from': dateFrom.toIso8601String(),
'date_to': dateTo.toIso8601String(),
'data': data.map((x) => x.toMap()).toList(),
};
}
}
class CategoryAnalyticItem {
final String categoryId;
final String categoryName;
final int totalRevenue;
final int totalQuantity;
final int productCount;
final int orderCount;
CategoryAnalyticItem({
required this.categoryId,
required this.categoryName,
required this.totalRevenue,
required this.totalQuantity,
required this.productCount,
required this.orderCount,
});
factory CategoryAnalyticItem.fromMap(Map<String, dynamic> map) {
return CategoryAnalyticItem(
categoryId: map['category_id'],
categoryName: map['category_name'],
totalRevenue: map['total_revenue'] ?? 0,
totalQuantity: map['total_quantity'] ?? 0,
productCount: map['product_count'] ?? 0,
orderCount: map['order_count'] ?? 0,
);
}
Map<String, dynamic> toMap() {
return {
'category_id': categoryId,
'category_name': categoryName,
'total_revenue': totalRevenue,
'total_quantity': totalQuantity,
'product_count': productCount,
'order_count': orderCount,
};
}
}
@@ -117,4 +117,14 @@ class CategoryModel {
'updated_at': updatedAt.toIso8601String(), 'updated_at': updatedAt.toIso8601String(),
}; };
} }
factory CategoryModel.all() => CategoryModel(
id: 'all',
organizationId: '',
name: 'Semua',
businessType: 'restaurant',
metadata: {},
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
} }
@@ -59,11 +59,17 @@ class DashboardAnalyticData {
dateFrom: map['date_from'], dateFrom: map['date_from'],
dateTo: map['date_to'], dateTo: map['date_to'],
overview: DashboardOverview.fromMap(map['overview']), overview: DashboardOverview.fromMap(map['overview']),
topProducts: List<TopProduct>.from( topProducts: map['top_products'] == null
? []
: List<TopProduct>.from(
map['top_products']?.map((x) => TopProduct.fromMap(x))), map['top_products']?.map((x) => TopProduct.fromMap(x))),
paymentMethods: List<PaymentMethodAnalytic>.from(map['payment_methods'] paymentMethods: map['payment_methods'] == null
? []
: List<PaymentMethodAnalytic>.from(map['payment_methods']
?.map((x) => PaymentMethodAnalytic.fromMap(x))), ?.map((x) => PaymentMethodAnalytic.fromMap(x))),
recentSales: List<RecentSale>.from( recentSales: map['recent_sales'] == null
? []
: List<RecentSale>.from(
map['recent_sales']?.map((x) => RecentSale.fromMap(x))), map['recent_sales']?.map((x) => RecentSale.fromMap(x))),
); );
@@ -0,0 +1,290 @@
class InventoryAnalyticResponseModel {
final bool success;
final InventoryAnalyticData? data;
final dynamic errors;
InventoryAnalyticResponseModel({
required this.success,
required this.data,
this.errors,
});
// From JSON
factory InventoryAnalyticResponseModel.fromJson(Map<String, dynamic> json) {
return InventoryAnalyticResponseModel(
success: json['success'],
data: json['data'] != null
? InventoryAnalyticData.fromMap(json['data'])
: null,
errors: json['errors'],
);
}
// To JSON
Map<String, dynamic> toJson() {
return {
'success': success,
'data': data?.toMap(),
'errors': errors,
};
}
// From Map
factory InventoryAnalyticResponseModel.fromMap(Map<String, dynamic> map) {
return InventoryAnalyticResponseModel(
success: map['success'],
data: map['data'] != null
? InventoryAnalyticData.fromMap(map['data'])
: null,
errors: map['errors'],
);
}
// To Map
Map<String, dynamic> toMap() {
return {
'success': success,
'data': data?.toMap(),
'errors': errors,
};
}
}
class InventoryAnalyticData {
final InventorySummary summary;
final List<InventoryProductItem> products;
final List<InventoryIngredientItem> ingredients;
InventoryAnalyticData({
required this.summary,
required this.products,
required this.ingredients,
});
factory InventoryAnalyticData.fromMap(Map<String, dynamic> map) {
return InventoryAnalyticData(
summary: InventorySummary.fromMap(map['summary']),
products: map['products'] == null
? []
: List<InventoryProductItem>.from(
map['products']?.map((x) => InventoryProductItem.fromMap(x)) ??
[],
),
ingredients: map['ingredients'] == null
? []
: List<InventoryIngredientItem>.from(
map['ingredients']
?.map((x) => InventoryIngredientItem.fromMap(x)) ??
[],
),
);
}
Map<String, dynamic> toMap() {
return {
'summary': summary.toMap(),
'products': products.map((x) => x.toMap()).toList(),
'ingredients': ingredients.map((x) => x.toMap()).toList(),
};
}
}
class InventorySummary {
final int totalProducts;
final int totalIngredients;
final int totalValue;
final int lowStockProducts;
final int lowStockIngredients;
final int zeroStockProducts;
final int zeroStockIngredients;
final int totalSoldProducts;
final int totalSoldIngredients;
final String outletId;
final String outletName;
final DateTime generatedAt;
InventorySummary({
required this.totalProducts,
required this.totalIngredients,
required this.totalValue,
required this.lowStockProducts,
required this.lowStockIngredients,
required this.zeroStockProducts,
required this.zeroStockIngredients,
required this.totalSoldProducts,
required this.totalSoldIngredients,
required this.outletId,
required this.outletName,
required this.generatedAt,
});
factory InventorySummary.fromMap(Map<String, dynamic> map) {
return InventorySummary(
totalProducts: map['total_products'] ?? 0,
totalIngredients: map['total_ingredients'] ?? 0,
totalValue: map['total_value'] ?? 0,
lowStockProducts: map['low_stock_products'] ?? 0,
lowStockIngredients: map['low_stock_ingredients'] ?? 0,
zeroStockProducts: map['zero_stock_products'] ?? 0,
zeroStockIngredients: map['zero_stock_ingredients'] ?? 0,
totalSoldProducts: map['total_sold_products'] ?? 0,
totalSoldIngredients: map['total_sold_ingredients'] ?? 0,
outletId: map['outlet_id'],
outletName: map['outlet_name'],
generatedAt: DateTime.parse(map['generated_at']),
);
}
Map<String, dynamic> toMap() {
return {
'total_products': totalProducts,
'total_ingredients': totalIngredients,
'total_value': totalValue,
'low_stock_products': lowStockProducts,
'low_stock_ingredients': lowStockIngredients,
'zero_stock_products': zeroStockProducts,
'zero_stock_ingredients': zeroStockIngredients,
'total_sold_products': totalSoldProducts,
'total_sold_ingredients': totalSoldIngredients,
'outlet_id': outletId,
'outlet_name': outletName,
'generated_at': generatedAt.toIso8601String(),
};
}
}
class InventoryProductItem {
final String id;
final String productId;
final String productName;
final String categoryName;
final int quantity;
final int reorderLevel;
final num unitCost;
final int totalValue;
final int totalIn;
final int totalOut;
final bool isLowStock;
final bool isZeroStock;
final DateTime updatedAt;
InventoryProductItem({
required this.id,
required this.productId,
required this.productName,
required this.categoryName,
required this.quantity,
required this.reorderLevel,
required this.unitCost,
required this.totalValue,
required this.totalIn,
required this.totalOut,
required this.isLowStock,
required this.isZeroStock,
required this.updatedAt,
});
factory InventoryProductItem.fromMap(Map<String, dynamic> map) {
return InventoryProductItem(
id: map['id'],
productId: map['product_id'],
productName: map['product_name'],
categoryName: map['category_name'],
quantity: map['quantity'] ?? 0,
reorderLevel: map['reorder_level'] ?? 0,
unitCost: map['unit_cost'] ?? 0,
totalValue: map['total_value'] ?? 0,
totalIn: map['total_in'] ?? 0,
totalOut: map['total_out'] ?? 0,
isLowStock: map['is_low_stock'] ?? false,
isZeroStock: map['is_zero_stock'] ?? false,
updatedAt: DateTime.parse(map['updated_at']),
);
}
Map<String, dynamic> toMap() {
return {
'id': id,
'product_id': productId,
'product_name': productName,
'category_name': categoryName,
'quantity': quantity,
'reorder_level': reorderLevel,
'unit_cost': unitCost,
'total_value': totalValue,
'total_in': totalIn,
'total_out': totalOut,
'is_low_stock': isLowStock,
'is_zero_stock': isZeroStock,
'updated_at': updatedAt.toIso8601String(),
};
}
}
class InventoryIngredientItem {
final String id;
final String ingredientId;
final String ingredientName;
final String unitName;
final int quantity;
final int reorderLevel;
final num unitCost;
final int totalValue;
final int totalIn;
final int totalOut;
final bool isLowStock;
final bool isZeroStock;
final DateTime updatedAt;
InventoryIngredientItem({
required this.id,
required this.ingredientId,
required this.ingredientName,
required this.unitName,
required this.quantity,
required this.reorderLevel,
required this.unitCost,
required this.totalValue,
required this.totalIn,
required this.totalOut,
required this.isLowStock,
required this.isZeroStock,
required this.updatedAt,
});
factory InventoryIngredientItem.fromMap(Map<String, dynamic> map) {
return InventoryIngredientItem(
id: map['id'],
ingredientId: map['ingredient_id'],
ingredientName: map['ingredient_name'],
unitName: map['unit_name'],
quantity: map['quantity'] ?? 0,
reorderLevel: map['reorder_level'] ?? 0,
unitCost: map['unit_cost'] ?? 0,
totalValue: map['total_value'] ?? 0,
totalIn: map['total_in'] ?? 0,
totalOut: map['total_out'] ?? 0,
isLowStock: map['is_low_stock'] ?? false,
isZeroStock: map['is_zero_stock'] ?? false,
updatedAt: DateTime.parse(map['updated_at']),
);
}
Map<String, dynamic> toMap() {
return {
'id': id,
'ingredient_id': ingredientId,
'ingredient_name': ingredientName,
'unit_name': unitName,
'quantity': quantity,
'reorder_level': reorderLevel,
'unit_cost': unitCost,
'total_value': totalValue,
'total_in': totalIn,
'total_out': totalOut,
'is_low_stock': isLowStock,
'is_zero_stock': isZeroStock,
'updated_at': updatedAt.toIso8601String(),
};
}
}
@@ -1,5 +1,8 @@
import 'dart:convert'; import 'dart:convert';
import 'package:enaklo_pos/data/models/response/product_response_model.dart';
import 'package:enaklo_pos/presentation/home/models/product_quantity.dart';
class OrderDetailResponseModel { class OrderDetailResponseModel {
final bool? success; final bool? success;
final Order? data; final Order? data;
@@ -81,7 +84,7 @@ class OrderData {
orders: map["orders"] == null orders: map["orders"] == null
? [] ? []
: List<Order>.from(map['orders']?.map((x) => Order.fromMap(x))), : List<Order>.from(map['orders']?.map((x) => Order.fromMap(x))),
payments: map["orders"] == null payments: map["payments"] == null
? [] ? []
: List<Payment>.from(map['payments']?.map((x) => Payment.fromMap(x))), : List<Payment>.from(map['payments']?.map((x) => Payment.fromMap(x))),
totalCount: map['total_count'], totalCount: map['total_count'],
@@ -115,7 +118,7 @@ class Order {
final int? taxAmount; final int? taxAmount;
final int? discountAmount; final int? discountAmount;
final int? totalAmount; final int? totalAmount;
final int? totalCost; final num? totalCost;
final int? remainingAmount; final int? remainingAmount;
final String? paymentStatus; final String? paymentStatus;
final int? refundAmount; final int? refundAmount;
@@ -129,6 +132,7 @@ class Order {
final List<Payment>? payments; final List<Payment>? payments;
final int? totalPaid; final int? totalPaid;
final int? paymentCount; final int? paymentCount;
final String? splitType;
Order({ Order({
this.id, this.id,
@@ -156,6 +160,7 @@ class Order {
this.payments, this.payments,
this.totalPaid, this.totalPaid,
this.paymentCount, this.paymentCount,
this.splitType,
}); });
factory Order.fromMap(Map<String, dynamic> map) { factory Order.fromMap(Map<String, dynamic> map) {
@@ -190,6 +195,7 @@ class Order {
: List<Payment>.from(map['payments'].map((x) => Payment.fromMap(x))), : List<Payment>.from(map['payments'].map((x) => Payment.fromMap(x))),
totalPaid: map['total_paid'], totalPaid: map['total_paid'],
paymentCount: map['payment_count'], paymentCount: map['payment_count'],
splitType: map['split_type'] ?? "",
); );
} }
@@ -220,6 +226,7 @@ class Order {
'payments': payments?.map((x) => x.toMap()).toList(), 'payments': payments?.map((x) => x.toMap()).toList(),
'total_paid': totalPaid, 'total_paid': totalPaid,
'payment_count': paymentCount, 'payment_count': paymentCount,
'split_type': splitType,
}; };
} }
} }
@@ -240,6 +247,7 @@ class OrderItem {
DateTime? createdAt; DateTime? createdAt;
DateTime? updatedAt; DateTime? updatedAt;
String? printerType; String? printerType;
int? paidQuantity;
OrderItem({ OrderItem({
this.id, this.id,
@@ -257,6 +265,7 @@ class OrderItem {
this.createdAt, this.createdAt,
this.updatedAt, this.updatedAt,
this.printerType, this.printerType,
this.paidQuantity,
}); });
factory OrderItem.fromMap(Map<String, dynamic> map) { factory OrderItem.fromMap(Map<String, dynamic> map) {
@@ -277,6 +286,7 @@ class OrderItem {
createdAt: DateTime.parse(map['created_at']), createdAt: DateTime.parse(map['created_at']),
updatedAt: DateTime.parse(map['updated_at']), updatedAt: DateTime.parse(map['updated_at']),
printerType: map['printer_type'], printerType: map['printer_type'],
paidQuantity: map['paid_quantity'],
); );
} }
@@ -297,6 +307,7 @@ class OrderItem {
'created_at': createdAt?.toIso8601String(), 'created_at': createdAt?.toIso8601String(),
'updated_at': updatedAt?.toIso8601String(), 'updated_at': updatedAt?.toIso8601String(),
'printer_type': printerType, 'printer_type': printerType,
'paid_quantity': paidQuantity,
}; };
} }
} }
@@ -372,3 +383,121 @@ class Payment {
}; };
} }
} }
extension OrderItemListExtension on List<OrderItem> {
List<ProductQuantity> toProductQuantities() => map((e) => ProductQuantity(
product: Product(
id: e.productId,
name: e.productName,
price: e.unitPrice,
printerType: e.printerType,
),
variant: ProductVariant(
id: e.productVariantId,
name: e.productVariantName,
productId: e.productId,
),
notes: e.notes ?? '',
quantity: e.quantity ?? 0,
)).toList();
}
extension OrderCopyWith on Order {
Order copyWith({
String? id,
String? orderNumber,
String? outletId,
String? userId,
String? tableNumber,
String? orderType,
String? status,
int? subtotal,
int? taxAmount,
int? discountAmount,
int? totalAmount,
num? totalCost,
int? remainingAmount,
String? paymentStatus,
int? refundAmount,
bool? isVoid,
bool? isRefund,
String? notes,
Map<String, dynamic>? metadata,
DateTime? createdAt,
DateTime? updatedAt,
List<OrderItem>? orderItems,
List<Payment>? payments,
int? totalPaid,
int? paymentCount,
String? splitType,
}) {
return Order(
id: id ?? this.id,
orderNumber: orderNumber ?? this.orderNumber,
outletId: outletId ?? this.outletId,
userId: userId ?? this.userId,
tableNumber: tableNumber ?? this.tableNumber,
orderType: orderType ?? this.orderType,
status: status ?? this.status,
subtotal: subtotal ?? this.subtotal,
taxAmount: taxAmount ?? this.taxAmount,
discountAmount: discountAmount ?? this.discountAmount,
totalAmount: totalAmount ?? this.totalAmount,
totalCost: totalCost ?? this.totalCost,
remainingAmount: remainingAmount ?? this.remainingAmount,
paymentStatus: paymentStatus ?? this.paymentStatus,
refundAmount: refundAmount ?? this.refundAmount,
isVoid: isVoid ?? this.isVoid,
isRefund: isRefund ?? this.isRefund,
notes: notes ?? this.notes,
metadata: metadata ?? this.metadata,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
orderItems: orderItems ?? this.orderItems,
payments: payments ?? this.payments,
totalPaid: totalPaid ?? this.totalPaid,
paymentCount: paymentCount ?? this.paymentCount,
splitType: splitType ?? this.splitType,
);
}
}
extension OrderItemCopyWith on OrderItem {
OrderItem copyWith({
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,
}) {
return OrderItem(
id: id ?? this.id,
orderId: orderId ?? this.orderId,
productId: productId ?? this.productId,
productName: productName ?? this.productName,
productVariantId: productVariantId ?? this.productVariantId,
productVariantName: productVariantName ?? this.productVariantName,
quantity: quantity ?? this.quantity,
unitPrice: unitPrice ?? this.unitPrice,
totalPrice: totalPrice ?? this.totalPrice,
modifiers: modifiers ?? this.modifiers,
notes: notes ?? this.notes,
status: status ?? this.status,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
printerType: printerType ?? this.printerType,
paidQuantity: paidQuantity ?? this.paidQuantity,
);
}
}
@@ -1,6 +1,6 @@
class PaymentMethodAnalyticResponseModel { class PaymentMethodAnalyticResponseModel {
final bool success; final bool success;
final PaymentMethodAnalyticData data; final PaymentMethodAnalyticData? data;
final dynamic errors; final dynamic errors;
PaymentMethodAnalyticResponseModel({ PaymentMethodAnalyticResponseModel({
@@ -18,7 +18,9 @@ class PaymentMethodAnalyticResponseModel {
factory PaymentMethodAnalyticResponseModel.fromMap(Map<String, dynamic> map) { factory PaymentMethodAnalyticResponseModel.fromMap(Map<String, dynamic> map) {
return PaymentMethodAnalyticResponseModel( return PaymentMethodAnalyticResponseModel(
success: map['success'], success: map['success'],
data: PaymentMethodAnalyticData.fromMap(map['data']), data: map['data'] == null
? null
: PaymentMethodAnalyticData.fromMap(map['data']),
errors: map['errors'], errors: map['errors'],
); );
} }
@@ -26,7 +28,7 @@ class PaymentMethodAnalyticResponseModel {
Map<String, dynamic> toMap() { Map<String, dynamic> toMap() {
return { return {
'success': success, 'success': success,
'data': data.toMap(), 'data': data?.toMap(),
'errors': errors, 'errors': errors,
}; };
} }
@@ -64,8 +66,11 @@ class PaymentMethodAnalyticData {
dateTo: DateTime.parse(map['date_to']), dateTo: DateTime.parse(map['date_to']),
groupBy: map['group_by'], groupBy: map['group_by'],
summary: PaymentSummary.fromMap(map['summary']), summary: PaymentSummary.fromMap(map['summary']),
data: List<PaymentMethodAnalyticItem>.from( data: map['data'] == null
map['data']?.map((x) => PaymentMethodAnalyticItem.fromMap(x)) ?? [], ? []
: List<PaymentMethodAnalyticItem>.from(
map['data']?.map((x) => PaymentMethodAnalyticItem.fromMap(x)) ??
[],
), ),
); );
} }
@@ -127,7 +132,7 @@ class PaymentMethodAnalyticItem {
final int totalAmount; final int totalAmount;
final int orderCount; final int orderCount;
final int paymentCount; final int paymentCount;
final int percentage; final num percentage;
PaymentMethodAnalyticItem({ PaymentMethodAnalyticItem({
required this.paymentMethodId, required this.paymentMethodId,
+120 -14
View File
@@ -5,6 +5,8 @@ class PrintModel {
final String address; final String address;
final String paper; final String paper;
final String type; final String type;
final DateTime? createdAt;
final DateTime? updatedAt;
PrintModel({ PrintModel({
this.id, this.id,
@@ -13,26 +15,130 @@ class PrintModel {
required this.address, required this.address,
required this.paper, required this.paper,
required this.type, required this.type,
this.createdAt,
this.updatedAt,
}); });
// from map // Factory constructor from map (updated)
factory PrintModel.fromMap(Map<String, dynamic> map) { factory PrintModel.fromMap(Map<String, dynamic> map) {
return PrintModel( return PrintModel(
id: map['id'], id: map['id'] as int?,
code: map['code'], code: map['code'] as String,
name: map['name'], name: map['name'] as String,
address: map['address'], address: map['address'] as String,
paper: map['paper'], paper: map['paper'] as String,
type: map['type'], type: map['type'] as String,
createdAt: map['created_at'] != null
? DateTime.tryParse(map['created_at'] as String)
: null,
updatedAt: map['updated_at'] != null
? DateTime.tryParse(map['updated_at'] as String)
: null,
); );
} }
// to map // Convert to map for database insertion (without id, with timestamps)
Map<String, dynamic> toMap() => { Map<String, dynamic> toMapForInsert() {
"code": code, final now = DateTime.now().toIso8601String();
"name": name, return {
"address": address, 'code': code,
"paper": paper, 'name': name,
"type": type, 'address': address,
'paper': paper,
'type': type,
'created_at': now,
'updated_at': now,
}; };
}
// Convert to map for database update (without id and created_at)
Map<String, dynamic> toMapForUpdate() {
return {
'code': code,
'name': name,
'address': address,
'paper': paper,
'type': type,
'updated_at': DateTime.now().toIso8601String(),
};
}
// Convert to complete map (original method, enhanced)
Map<String, dynamic> toMap() {
return {
if (id != null) 'id': id,
'code': code,
'name': name,
'address': address,
'paper': paper,
'type': type,
if (createdAt != null) 'created_at': createdAt!.toIso8601String(),
if (updatedAt != null) 'updated_at': updatedAt!.toIso8601String(),
};
}
// Copy with method for creating modified instances
PrintModel copyWith({
int? id,
String? code,
String? name,
String? address,
String? paper,
String? type,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return PrintModel(
id: id ?? this.id,
code: code ?? this.code,
name: name ?? this.name,
address: address ?? this.address,
paper: paper ?? this.paper,
type: type ?? this.type,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
// Equality and hashCode
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is PrintModel &&
other.id == id &&
other.code == code &&
other.name == name &&
other.address == address &&
other.paper == paper &&
other.type == type;
}
@override
int get hashCode {
return Object.hash(id, code, name, address, paper, type);
}
// String representation for debugging (matches datasource logging)
@override
String toString() {
return 'PrintModel(id: $id, code: $code, name: $name, address: $address, paper: $paper, type: $type, createdAt: $createdAt, updatedAt: $updatedAt)';
}
// Validation methods
bool get isValid {
return code.isNotEmpty &&
name.isNotEmpty &&
address.isNotEmpty &&
paper.isNotEmpty &&
type.isNotEmpty;
}
String? get validationError {
if (code.isEmpty) return 'Printer code cannot be empty';
if (name.isEmpty) return 'Printer name cannot be empty';
if (address.isEmpty) return 'Printer address cannot be empty';
if (paper.isEmpty) return 'Paper size cannot be empty';
if (type.isEmpty) return 'Printer type cannot be empty';
return null;
}
} }
@@ -1,6 +1,6 @@
class ProductAnalyticResponseModel { class ProductAnalyticResponseModel {
final bool success; final bool success;
final ProductAnalyticData data; final ProductAnalyticData? data;
final dynamic errors; final dynamic errors;
ProductAnalyticResponseModel({ ProductAnalyticResponseModel({
@@ -17,7 +17,8 @@ class ProductAnalyticResponseModel {
factory ProductAnalyticResponseModel.fromMap(Map<String, dynamic> map) { factory ProductAnalyticResponseModel.fromMap(Map<String, dynamic> map) {
return ProductAnalyticResponseModel( return ProductAnalyticResponseModel(
success: map['success'] ?? false, success: map['success'] ?? false,
data: ProductAnalyticData.fromMap(map['data']), data:
map['data'] == null ? null : ProductAnalyticData.fromMap(map['data']),
errors: map['errors'], errors: map['errors'],
); );
} }
@@ -25,7 +26,7 @@ class ProductAnalyticResponseModel {
Map<String, dynamic> toMap() { Map<String, dynamic> toMap() {
return { return {
'success': success, 'success': success,
'data': data.toMap(), 'data': data?.toMap(),
'errors': errors, 'errors': errors,
}; };
} }
@@ -52,7 +53,9 @@ class ProductAnalyticData {
outletId: map['outlet_id'], outletId: map['outlet_id'],
dateFrom: DateTime.parse(map['date_from']), dateFrom: DateTime.parse(map['date_from']),
dateTo: DateTime.parse(map['date_to']), dateTo: DateTime.parse(map['date_to']),
data: List<ProductAnalyticItem>.from( data: map['data'] == null
? []
: List<ProductAnalyticItem>.from(
map['data'].map((x) => ProductAnalyticItem.fromMap(x)), map['data'].map((x) => ProductAnalyticItem.fromMap(x)),
), ),
); );
@@ -1,6 +1,14 @@
// 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';
int? parseInt(dynamic value) {
if (value == null) return null;
if (value is int) return value;
if (value is double) return value.toInt();
if (value is String) return int.tryParse(value);
return null;
}
class ProductResponseModel { class ProductResponseModel {
final bool? success; final bool? success;
final ProductData? data; final ProductData? data;
@@ -51,10 +59,10 @@ class ProductData {
? [] ? []
: List<Product>.from( : List<Product>.from(
json["products"].map((x) => Product.fromMap(x))), json["products"].map((x) => Product.fromMap(x))),
totalCount: json["total_count"], totalCount: parseInt(json["total_count"]),
page: json["page"], page: parseInt(json["page"]),
limit: json["limit"], limit: parseInt(json["limit"]),
totalPages: json["total_pages"], totalPages: parseInt(json["total_pages"]),
); );
Map<String, dynamic> toMap() => { Map<String, dynamic> toMap() => {
@@ -116,8 +124,8 @@ class Product {
sku: json["sku"], sku: json["sku"],
name: json["name"], name: json["name"],
description: json["description"], description: json["description"],
price: json["price"], price: parseInt(json["price"]),
cost: json["cost"], cost: parseInt(json["cost"]),
businessType: json["business_type"], businessType: json["business_type"],
imageUrl: json["image_url"], imageUrl: json["image_url"],
printerType: json["printer_type"], printerType: json["printer_type"],
@@ -137,7 +145,7 @@ class Product {
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"], price: parseInt(json["price"]),
); );
factory Product.fromLocalMap(Map<String, dynamic> json) => Product( factory Product.fromLocalMap(Map<String, dynamic> json) => Product(
@@ -147,8 +155,8 @@ class Product {
sku: json["sku"], sku: json["sku"],
name: json["name"], name: json["name"],
description: json["description"], description: json["description"],
price: json["price"], price: parseInt(json["price"]),
cost: json["cost"], cost: parseInt(json["cost"]),
businessType: json["business_type"], businessType: json["business_type"],
imageUrl: json["image_url"], imageUrl: json["image_url"],
printerType: json["printer_type"], printerType: json["printer_type"],
@@ -207,96 +215,6 @@ class Product {
? [] ? []
: List<dynamic>.from(variants!.map((x) => x.toMap())), : List<dynamic>.from(variants!.map((x) => x.toMap())),
}; };
@override
bool operator ==(covariant Product other) {
if (identical(this, other)) return true;
return other.id == id &&
other.organizationId == organizationId &&
other.categoryId == categoryId &&
other.sku == sku &&
other.name == name &&
other.description == description &&
other.price == price &&
other.cost == cost &&
other.businessType == businessType &&
other.imageUrl == imageUrl &&
other.printerType == printerType &&
other.metadata == metadata &&
other.isActive == isActive &&
other.createdAt == createdAt &&
other.updatedAt == updatedAt &&
_listEquals(other.variants, variants);
}
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
int get hashCode {
return id.hashCode ^
organizationId.hashCode ^
categoryId.hashCode ^
sku.hashCode ^
name.hashCode ^
description.hashCode ^
price.hashCode ^
cost.hashCode ^
businessType.hashCode ^
imageUrl.hashCode ^
printerType.hashCode ^
metadata.hashCode ^
isActive.hashCode ^
createdAt.hashCode ^
updatedAt.hashCode ^
variants.hashCode;
}
Product copyWith({
String? id,
String? organizationId,
String? categoryId,
String? sku,
String? name,
String? description,
int? price,
int? cost,
String? businessType,
String? imageUrl,
String? printerType,
Map<String, dynamic>? metadata,
bool? isActive,
DateTime? createdAt,
DateTime? updatedAt,
List<ProductVariant>? variants,
}) {
return Product(
id: id ?? this.id,
organizationId: organizationId ?? this.organizationId,
categoryId: categoryId ?? this.categoryId,
sku: sku ?? this.sku,
name: name ?? this.name,
description: description ?? this.description,
price: price ?? this.price,
cost: cost ?? this.cost,
businessType: businessType ?? this.businessType,
imageUrl: imageUrl ?? this.imageUrl,
printerType: printerType ?? this.printerType,
metadata: metadata ?? this.metadata,
isActive: isActive ?? this.isActive,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
variants: variants ?? this.variants,
);
}
} }
class Category { class Category {
@@ -341,28 +259,6 @@ class Category {
"created_at": createdAt?.toIso8601String(), "created_at": createdAt?.toIso8601String(),
"updated_at": updatedAt?.toIso8601String(), "updated_at": updatedAt?.toIso8601String(),
}; };
@override
bool operator ==(covariant Category other) {
if (identical(this, other)) return true;
return other.id == id &&
other.name == name &&
other.description == description &&
other.image == image &&
other.createdAt == createdAt &&
other.updatedAt == updatedAt;
}
@override
int get hashCode {
return id.hashCode ^
name.hashCode ^
description.hashCode ^
image.hashCode ^
createdAt.hashCode ^
updatedAt.hashCode;
}
} }
class ProductVariant { class ProductVariant {
@@ -390,8 +286,8 @@ class ProductVariant {
id: json["id"], id: json["id"],
productId: json["product_id"], productId: json["product_id"],
name: json["name"], name: json["name"],
priceModifier: json["price_modifier"], priceModifier: parseInt(json["price_modifier"]),
cost: json["cost"], cost: parseInt(json["cost"]),
metadata: json["metadata"] ?? {}, metadata: json["metadata"] ?? {},
createdAt: json["created_at"] == null createdAt: json["created_at"] == null
? null ? null
@@ -1,6 +1,6 @@
class ProfitLossResponseModel { class ProfitLossResponseModel {
final bool success; final bool success;
final ProfitLossData data; final ProfitLossData? data;
final dynamic errors; final dynamic errors;
ProfitLossResponseModel({ ProfitLossResponseModel({
@@ -13,7 +13,7 @@ class ProfitLossResponseModel {
factory ProfitLossResponseModel.fromJson(Map<String, dynamic> json) { factory ProfitLossResponseModel.fromJson(Map<String, dynamic> json) {
return ProfitLossResponseModel( return ProfitLossResponseModel(
success: json['success'], success: json['success'],
data: ProfitLossData.fromMap(json['data']), data: json['data'] == null ? null : ProfitLossData.fromMap(json['data']),
errors: json['errors'], errors: json['errors'],
); );
} }
@@ -22,7 +22,7 @@ class ProfitLossResponseModel {
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return { return {
'success': success, 'success': success,
'data': data.toMap(), 'data': data?.toMap(),
'errors': errors, 'errors': errors,
}; };
} }
@@ -40,7 +40,7 @@ class ProfitLossResponseModel {
Map<String, dynamic> toMap() { Map<String, dynamic> toMap() {
return { return {
'success': success, 'success': success,
'data': data.toMap(), 'data': data?.toMap(),
'errors': errors, 'errors': errors,
}; };
} }
@@ -72,9 +72,13 @@ class ProfitLossData {
dateTo: map['date_to'], dateTo: map['date_to'],
groupBy: map['group_by'], groupBy: map['group_by'],
summary: ProfitLossSummary.fromMap(map['summary']), summary: ProfitLossSummary.fromMap(map['summary']),
data: List<ProfitLossItem>.from( data: map['data'] == null
? []
: List<ProfitLossItem>.from(
map['data'].map((x) => ProfitLossItem.fromMap(x))), map['data'].map((x) => ProfitLossItem.fromMap(x))),
productData: List<ProfitLossProduct>.from( productData: map['product_data'] == null
? []
: List<ProfitLossProduct>.from(
map['product_data'].map((x) => ProfitLossProduct.fromMap(x))), map['product_data'].map((x) => ProfitLossProduct.fromMap(x))),
); );
} }
@@ -94,15 +98,15 @@ class ProfitLossData {
class ProfitLossSummary { class ProfitLossSummary {
final int totalRevenue; final int totalRevenue;
final int totalCost; final num totalCost;
final int grossProfit; final num grossProfit;
final double grossProfitMargin; final double grossProfitMargin;
final int totalTax; final int totalTax;
final int totalDiscount; final int totalDiscount;
final int netProfit; final num netProfit;
final double netProfitMargin; final double netProfitMargin;
final int totalOrders; final int totalOrders;
final int averageProfit; final double averageProfit;
final double profitabilityRatio; final double profitabilityRatio;
ProfitLossSummary({ ProfitLossSummary({
@@ -130,7 +134,7 @@ class ProfitLossSummary {
netProfit: map['net_profit'], netProfit: map['net_profit'],
netProfitMargin: (map['net_profit_margin'] as num).toDouble(), netProfitMargin: (map['net_profit_margin'] as num).toDouble(),
totalOrders: map['total_orders'], totalOrders: map['total_orders'],
averageProfit: map['average_profit'], averageProfit: (map['average_profit'] as num).toDouble(),
profitabilityRatio: (map['profitability_ratio'] as num).toDouble(), profitabilityRatio: (map['profitability_ratio'] as num).toDouble(),
); );
} }
@@ -155,12 +159,12 @@ class ProfitLossSummary {
class ProfitLossItem { class ProfitLossItem {
final String date; final String date;
final int revenue; final int revenue;
final int cost; final num cost;
final int grossProfit; final num grossProfit;
final double grossProfitMargin; final double grossProfitMargin;
final int tax; final int tax;
final int discount; final int discount;
final int netProfit; final num netProfit;
final double netProfitMargin; final double netProfitMargin;
final int orders; final int orders;
@@ -215,12 +219,12 @@ class ProfitLossProduct {
final String categoryName; final String categoryName;
final int quantitySold; final int quantitySold;
final int revenue; final int revenue;
final int cost; final num cost;
final int grossProfit; final num grossProfit;
final double grossProfitMargin; final double grossProfitMargin;
final int averagePrice; final int averagePrice;
final int averageCost; final num averageCost;
final int profitPerUnit; final num profitPerUnit;
ProfitLossProduct({ ProfitLossProduct({
required this.productId, required this.productId,
@@ -248,8 +252,8 @@ class ProfitLossProduct {
cost: map['cost'], cost: map['cost'],
grossProfit: map['gross_profit'], grossProfit: map['gross_profit'],
grossProfitMargin: (map['gross_profit_margin'] as num).toDouble(), grossProfitMargin: (map['gross_profit_margin'] as num).toDouble(),
averagePrice: map['average_price'], averagePrice: (map['average_price'] as num).toInt(),
averageCost: map['average_cost'], averageCost: (map['average_cost'] as num).toInt(),
profitPerUnit: map['profit_per_unit'], profitPerUnit: map['profit_per_unit'],
); );
} }
@@ -0,0 +1,297 @@
import 'dart:developer';
import 'package:dartz/dartz.dart';
import 'package:enaklo_pos/data/datasources/category/category_local_datasource.dart';
import 'package:enaklo_pos/data/datasources/category/category_remote_datasource.dart';
import 'package:enaklo_pos/data/models/response/category_response_model.dart';
class CategoryRepository {
static CategoryRepository? _instance;
final CategoryLocalDatasource _localDatasource;
final CategoryRemoteDatasource _remoteDatasource;
CategoryRepository._internal()
: _localDatasource = CategoryLocalDatasource.instance,
_remoteDatasource = CategoryRemoteDatasource();
static CategoryRepository get instance {
_instance ??= CategoryRepository._internal();
return _instance!;
}
// ========================================
// SYNC STRATEGY: REMOTE-FIRST WITH LOCAL FALLBACK
// ========================================
Future<Either<String, CategoryResponseModel>> getCategories({
int page = 1,
int limit = 10,
bool isActive = true,
String? search,
bool forceRemote = false,
}) async {
try {
log('📱 Getting categories - page: $page, isActive: $isActive, search: $search, forceRemote: $forceRemote');
// Clean expired cache
_localDatasource.clearExpiredCache();
// Check if we should try remote first
if (forceRemote || !await _localDatasource.hasCategories()) {
log('🌐 Attempting remote fetch first...');
final remoteResult = await _getRemoteCategories(
page: page,
limit: limit,
isActive: isActive,
);
return await remoteResult.fold(
(failure) async {
log('❌ Remote fetch failed: $failure');
log('📱 Falling back to local data...');
return _getLocalCategories(
page: page,
limit: limit,
isActive: isActive,
search: search,
);
},
(response) async {
log('✅ Remote fetch successful, syncing to local...');
// Sync remote data to local
if (response.data.categories.isNotEmpty) {
await _syncToLocal(response.data.categories,
clearFirst: page == 1);
}
return Right(response);
},
);
} else {
log('📱 Using local data (cache available)...');
return _getLocalCategories(
page: page,
limit: limit,
isActive: isActive,
search: search,
);
}
} catch (e) {
log('❌ Error in getCategories: $e');
return Left('Gagal memuat kategori: $e');
}
}
// ========================================
// PURE LOCAL OPERATIONS
// ========================================
Future<Either<String, CategoryResponseModel>> _getLocalCategories({
int page = 1,
int limit = 10,
bool isActive = true,
String? search,
}) async {
try {
final cachedCategories = await _localDatasource.getCachedCategories(
page: page,
limit: limit,
isActive: isActive,
search: search,
);
final totalCount = await _localDatasource.getTotalCount(
isActive: isActive,
search: search,
);
final categoryData = CategoryData(
categories: cachedCategories,
totalCount: totalCount,
page: page,
limit: limit,
totalPages: totalCount > 0 ? (totalCount / limit).ceil() : 0,
);
final response = CategoryResponseModel(
success: true,
data: categoryData,
);
log('✅ Returned ${cachedCategories.length} local categories (${totalCount} total)');
return Right(response);
} catch (e) {
log('❌ Error getting local categories: $e');
return Left('Gagal memuat kategori dari database lokal: $e');
}
}
// ========================================
// REMOTE FETCH
// ========================================
Future<Either<String, CategoryResponseModel>> _getRemoteCategories({
int page = 1,
int limit = 10,
bool isActive = true,
}) async {
try {
log('🌐 Fetching categories from remote...');
return await _remoteDatasource.getCategories(
page: page,
limit: limit,
isActive: isActive,
);
} catch (e) {
log('❌ Remote fetch error: $e');
return Left('Gagal mengambil data dari server: $e');
}
}
// ========================================
// SYNC TO LOCAL
// ========================================
Future<void> _syncToLocal(List<CategoryModel> categories,
{bool clearFirst = false}) async {
try {
log('💾 Syncing ${categories.length} categories to local database...');
await _localDatasource.saveCategoriesBatch(categories,
clearFirst: clearFirst);
log('✅ Categories synced to local successfully');
} catch (e) {
log('❌ Error syncing categories to local: $e');
rethrow;
}
}
// ========================================
// MANUAL SYNC OPERATIONS
// ========================================
Future<Either<String, String>> syncAllCategories() async {
try {
log('🔄 Starting manual sync of all categories...');
int page = 1;
const limit = 50; // Higher limit for bulk sync
bool hasMore = true;
int totalSynced = 0;
// Clear local data first for fresh sync
await _localDatasource.clearAllCategories();
while (hasMore) {
log('📄 Syncing page $page...');
final result = await _remoteDatasource.getCategories(
page: page,
limit: limit,
isActive: true,
);
await result.fold(
(failure) async {
log('❌ Sync failed at page $page: $failure');
throw Exception(failure);
},
(response) async {
final categories = response.data.categories;
if (categories.isNotEmpty) {
await _localDatasource.saveCategoriesBatch(
categories,
clearFirst: false, // Don't clear on subsequent pages
);
totalSynced += categories.length;
// Check if we have more pages
hasMore = page < response.data.totalPages;
page++;
log('📦 Page $page synced: ${categories.length} categories');
} else {
hasMore = false;
}
},
);
}
final message = 'Berhasil sinkronisasi $totalSynced kategori';
log('$message');
return Right(message);
} catch (e) {
final error = 'Gagal sinkronisasi kategori: $e';
log('$error');
return Left(error);
}
}
// ========================================
// UTILITY METHODS
// ========================================
Future<Either<String, CategoryResponseModel>> refreshCategories({
bool isActive = true,
String? search,
}) async {
log('🔄 Refreshing categories...');
clearCache();
return await getCategories(
page: 1,
limit: 10,
isActive: isActive,
search: search,
forceRemote: true, // Force remote refresh
);
}
Future<CategoryModel?> getCategoryById(String id) async {
log('🔍 Getting category by ID: $id');
return await _localDatasource.getCategoryById(id);
}
Future<List<CategoryModel>> getAllCategories() async {
log('📋 Getting all categories for dropdown...');
return await _localDatasource.getAllCategories();
}
Future<bool> hasLocalCategories() async {
final hasCategories = await _localDatasource.hasCategories();
log('📊 Has local categories: $hasCategories');
return hasCategories;
}
Future<Map<String, dynamic>> getDatabaseStats() async {
final stats = await _localDatasource.getDatabaseStats();
log('📊 Category database stats: $stats');
return stats;
}
void clearCache() {
log('🧹 Clearing category cache');
_localDatasource.clearCache();
}
Future<bool> isLocalDatabaseReady() async {
try {
final stats = await getDatabaseStats();
final categoryCount = stats['total_categories'] ?? 0;
final isReady = categoryCount > 0;
log('🔍 Category database ready: $isReady ($categoryCount categories)');
return isReady;
} catch (e) {
log('❌ Error checking category database readiness: $e');
return false;
}
}
Future<void> clearAllCategories() async {
try {
log('🗑️ Clearing all categories from repository...');
await _localDatasource.clearAllCategories();
clearCache();
log('✅ All categories cleared successfully');
} catch (e) {
log('❌ Error clearing all categories: $e');
rethrow;
}
}
}
@@ -0,0 +1,220 @@
import 'dart:developer';
import 'package:dartz/dartz.dart';
import 'package:enaklo_pos/data/datasources/product/product_local_datasource.dart';
import 'package:enaklo_pos/data/datasources/product_remote_datasource.dart';
import 'package:enaklo_pos/data/models/response/product_response_model.dart';
class ProductRepository {
static ProductRepository? _instance;
final ProductLocalDatasource _localDatasource;
final ProductRemoteDatasource _remoteDatasource;
ProductRepository._internal()
: _localDatasource = ProductLocalDatasource.instance,
_remoteDatasource = ProductRemoteDatasource();
static ProductRepository get instance {
_instance ??= ProductRepository._internal();
return _instance!;
}
// ========================================
// PURE LOCAL DATABASE OPERATIONS
// ========================================
Future<Either<String, ProductResponseModel>> getProducts({
int page = 1,
int limit = 10,
String? categoryId,
String? search,
bool forceRefresh = false, // Ignored - kept for compatibility
}) async {
try {
log('📱 Getting products from local database - page: $page, categoryId: $categoryId, search: $search');
// Clean expired cache for optimal performance
_localDatasource.clearExpiredCache();
// Use cached query for maximum performance
final cachedProducts = await _localDatasource.getCachedProducts(
page: page,
limit: limit,
categoryId: categoryId,
search: search,
);
final totalCount = await _localDatasource.getTotalCount(
categoryId: categoryId,
search: search,
);
final productData = ProductData(
products: cachedProducts,
totalCount: totalCount,
page: page,
limit: limit,
totalPages: totalCount > 0 ? (totalCount / limit).ceil() : 0,
);
final response = ProductResponseModel(
success: true,
data: productData,
errors: null,
);
log('✅ Returned ${cachedProducts.length} local products (${totalCount} total)');
return Right(response);
} catch (e) {
log('❌ Error getting local products: $e');
return Left('Gagal memuat produk dari database lokal: $e');
}
}
// ========================================
// OPTIMIZED LOCAL SEARCH
// ========================================
Future<Either<String, List<Product>>> searchProductsOptimized(
String query) async {
try {
log('🔍 Local optimized search for: "$query"');
final products = await _localDatasource.searchProductsOptimized(query);
log('✅ Local search completed: ${products.length} results');
return Right(products);
} catch (e) {
log('❌ Error in local search: $e');
return Left('Pencarian lokal gagal: $e');
}
}
// ========================================
// PRODUCT SYNC OPERATIONS
// ========================================
Future<Either<String, String>> syncAllProducts() async {
try {
log('🔄 Starting manual sync of all products...');
int page = 1;
const limit = 50; // Higher limit for bulk sync
bool hasMore = true;
int totalSynced = 0;
// Clear local data first for fresh sync
await _localDatasource.clearAllProducts();
while (hasMore) {
log('📄 Syncing page $page...');
final result = await _remoteDatasource.getProducts(
page: page,
limit: limit,
);
await result.fold(
(failure) async {
log('❌ Sync failed at page $page: $failure');
throw Exception(failure);
},
(response) async {
final products = response.data?.products ?? [];
if (products.isNotEmpty) {
await _localDatasource.saveProductsBatch(
products,
clearFirst: false, // Don't clear on subsequent pages
);
totalSynced += products.length;
// Check if we have more pages
hasMore = page < (response.data?.totalPages ?? 0);
page++;
log('📦 Page $page synced: ${products.length} products');
} else {
hasMore = false;
}
},
);
}
final message = 'Berhasil sinkronisasi $totalSynced produk';
log('$message');
return Right(message);
} catch (e) {
final error = 'Gagal sinkronisasi produk: $e';
log('$error');
return Left(error);
}
}
// ========================================
// LOCAL DATABASE OPERATIONS
// ========================================
// Refresh just cleans cache and reloads from local
Future<Either<String, ProductResponseModel>> refreshProducts({
String? categoryId,
String? search,
}) async {
log('🔄 Refreshing local products...');
// Clear cache for fresh local data
clearCache();
return await getProducts(
page: 1,
limit: 10,
categoryId: categoryId,
search: search,
);
}
Future<Product?> getProductById(String id) async {
log('🔍 Getting product by ID from local: $id');
return await _localDatasource.getProductById(id);
}
Future<bool> hasLocalProducts() async {
final hasProducts = await _localDatasource.hasProducts();
log('📊 Has local products: $hasProducts');
return hasProducts;
}
Future<Map<String, dynamic>> getDatabaseStats() async {
final stats = await _localDatasource.getDatabaseStats();
log('📊 Database stats: $stats');
return stats;
}
void clearCache() {
log('🧹 Clearing local cache');
_localDatasource.clearCache();
}
// Helper method to check if local database is populated
Future<bool> isLocalDatabaseReady() async {
try {
final stats = await getDatabaseStats();
final productCount = stats['total_products'] ?? 0;
final isReady = productCount > 0;
log('🔍 Local database ready: $isReady ($productCount products)');
return isReady;
} catch (e) {
log('❌ Error checking database readiness: $e');
return false;
}
}
Future<void> clearAllProducts() async {
try {
log('🗑️ Clearing all products from repository...');
await _localDatasource.clearAllProducts();
clearCache();
log('✅ All products cleared successfully');
} catch (e) {
log('❌ Error clearing all products: $e');
rethrow;
}
}
}
+79
View File
@@ -0,0 +1,79 @@
import 'dart:async';
import 'dart:developer';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:enaklo_pos/data/repositories/product/product_repository.dart';
class SyncManager {
final ProductRepository _productRepository;
final Connectivity _connectivity = Connectivity();
Timer? _syncTimer;
bool _isSyncing = false;
StreamSubscription<List<ConnectivityResult>>? _connectivitySubscription;
SyncManager(this._productRepository) {
_startPeriodicSync();
_listenToConnectivityChanges();
}
void _startPeriodicSync() {
// Sync setiap 5 menit jika ada koneksi
_syncTimer = Timer.periodic(Duration(minutes: 5), (timer) {
_performBackgroundSync();
});
}
void _listenToConnectivityChanges() {
_connectivitySubscription = _connectivity.onConnectivityChanged.listen(
(List<ConnectivityResult> results) {
// Check if any connection is available
final hasConnection =
results.any((result) => result != ConnectivityResult.none);
if (hasConnection) {
log('Connection restored, starting background sync');
_performBackgroundSync();
} else {
log('Connection lost');
}
},
);
}
Future<void> _performBackgroundSync() async {
if (_isSyncing) return;
// Check current connectivity before syncing
final connectivityResults = await _connectivity.checkConnectivity();
final hasConnection =
connectivityResults.any((result) => result != ConnectivityResult.none);
if (!hasConnection) {
log('No internet connection, skipping sync');
return;
}
try {
_isSyncing = true;
log('Starting background sync');
await _productRepository.refreshProducts();
log('Background sync completed');
} catch (e) {
log('Background sync failed: $e');
} finally {
_isSyncing = false;
}
}
// Public method untuk manual sync
Future<void> performManualSync() async {
await _performBackgroundSync();
}
void dispose() {
_syncTimer?.cancel();
_connectivitySubscription?.cancel();
}
}
+48 -37
View File
@@ -1,14 +1,17 @@
import 'dart:async';
import 'dart:developer'; import 'dart:developer';
import 'dart:ui';
import 'package:enaklo_pos/core/constants/theme.dart'; import 'package:enaklo_pos/core/constants/theme.dart';
import 'package:enaklo_pos/core/network/dio_client.dart'; import 'package:enaklo_pos/core/network/dio_client.dart';
import 'package:enaklo_pos/data/datasources/analytic_remote_datasource.dart'; import 'package:enaklo_pos/data/datasources/analytic_remote_datasource.dart';
import 'package:enaklo_pos/data/datasources/customer_remote_datasource.dart'; import 'package:enaklo_pos/data/datasources/customer_remote_datasource.dart';
import 'package:enaklo_pos/data/datasources/file_remote_datasource.dart'; import 'package:enaklo_pos/data/datasources/file_remote_datasource.dart';
import 'package:enaklo_pos/data/datasources/outlet_remote_data_source.dart'; import 'package:enaklo_pos/data/datasources/outlet/outlet_remote_data_source.dart';
import 'package:enaklo_pos/data/datasources/table_remote_datasource.dart'; import 'package:enaklo_pos/data/datasources/table_remote_datasource.dart';
import 'package:enaklo_pos/data/datasources/user_remote_datasource.dart'; import 'package:enaklo_pos/data/datasources/user_remote_datasource.dart';
import 'package:enaklo_pos/presentation/customer/bloc/customer_form/customer_form_bloc.dart'; import 'package:enaklo_pos/presentation/customer/bloc/customer_form/customer_form_bloc.dart';
import 'package:enaklo_pos/presentation/customer/bloc/customer_loader/customer_loader_bloc.dart'; import 'package:enaklo_pos/presentation/customer/bloc/customer_loader/customer_loader_bloc.dart';
import 'package:enaklo_pos/presentation/data_sync/bloc/data_sync_bloc.dart';
import 'package:enaklo_pos/presentation/home/bloc/category_loader/category_loader_bloc.dart'; import 'package:enaklo_pos/presentation/home/bloc/category_loader/category_loader_bloc.dart';
import 'package:enaklo_pos/presentation/home/bloc/current_outlet/current_outlet_bloc.dart'; import 'package:enaklo_pos/presentation/home/bloc/current_outlet/current_outlet_bloc.dart';
import 'package:enaklo_pos/presentation/home/bloc/order_form/order_form_bloc.dart'; import 'package:enaklo_pos/presentation/home/bloc/order_form/order_form_bloc.dart';
@@ -16,20 +19,25 @@ import 'package:enaklo_pos/presentation/home/bloc/outlet_loader/outlet_loader_bl
import 'package:enaklo_pos/presentation/home/bloc/product_loader/product_loader_bloc.dart'; import 'package:enaklo_pos/presentation/home/bloc/product_loader/product_loader_bloc.dart';
import 'package:enaklo_pos/presentation/home/bloc/user_update_outlet/user_update_outlet_bloc.dart'; import 'package:enaklo_pos/presentation/home/bloc/user_update_outlet/user_update_outlet_bloc.dart';
import 'package:enaklo_pos/presentation/refund/bloc/refund_bloc.dart'; import 'package:enaklo_pos/presentation/refund/bloc/refund_bloc.dart';
import 'package:enaklo_pos/presentation/report/blocs/category_report/category_report_bloc.dart';
import 'package:enaklo_pos/presentation/report/blocs/inventory_report/inventory_report_bloc.dart';
import 'package:enaklo_pos/presentation/report/blocs/profit_loss/profit_loss_bloc.dart'; import 'package:enaklo_pos/presentation/report/blocs/profit_loss/profit_loss_bloc.dart';
import 'package:enaklo_pos/presentation/report/blocs/report/report_bloc.dart';
import 'package:enaklo_pos/presentation/sales/blocs/order_loader/order_loader_bloc.dart'; import 'package:enaklo_pos/presentation/sales/blocs/order_loader/order_loader_bloc.dart';
import 'package:enaklo_pos/presentation/sales/blocs/payment_form/payment_form_bloc.dart'; import 'package:enaklo_pos/presentation/sales/blocs/payment_form/payment_form_bloc.dart';
import 'package:enaklo_pos/presentation/setting/bloc/get_printer_ticket/get_printer_ticket_bloc.dart'; import 'package:enaklo_pos/presentation/setting/bloc/get_printer_ticket/get_printer_ticket_bloc.dart';
import 'package:enaklo_pos/presentation/setting/bloc/upload_file/upload_file_bloc.dart'; import 'package:enaklo_pos/presentation/setting/bloc/upload_file/upload_file_bloc.dart';
import 'package:enaklo_pos/presentation/table/blocs/transfer_table/transfer_table_bloc.dart';
import 'package:enaklo_pos/presentation/void/bloc/void_order_bloc.dart'; import 'package:enaklo_pos/presentation/void/bloc/void_order_bloc.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.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/datasources/auth_remote_datasource.dart'; import 'package:enaklo_pos/data/datasources/auth_remote_datasource.dart';
import 'package:enaklo_pos/data/datasources/category_remote_datasource.dart'; import 'package:enaklo_pos/data/datasources/category/category_remote_datasource.dart';
import 'package:enaklo_pos/data/datasources/discount_remote_datasource.dart'; import 'package:enaklo_pos/data/datasources/discount_remote_datasource.dart';
import 'package:enaklo_pos/data/datasources/midtrans_remote_datasource.dart'; import 'package:enaklo_pos/data/datasources/midtrans_remote_datasource.dart';
import 'package:enaklo_pos/data/datasources/order_remote_datasource.dart'; import 'package:enaklo_pos/data/datasources/order_remote_datasource.dart';
import 'package:enaklo_pos/data/datasources/product_local_datasource.dart';
import 'package:enaklo_pos/data/datasources/product_remote_datasource.dart'; import 'package:enaklo_pos/data/datasources/product_remote_datasource.dart';
import 'package:enaklo_pos/data/datasources/payment_methods_remote_datasource.dart'; import 'package:enaklo_pos/data/datasources/payment_methods_remote_datasource.dart';
import 'package:enaklo_pos/data/datasources/settings_local_datasource.dart'; import 'package:enaklo_pos/data/datasources/settings_local_datasource.dart';
@@ -39,14 +47,11 @@ import 'package:enaklo_pos/presentation/home/bloc/get_table_status/get_table_sta
import 'package:enaklo_pos/presentation/home/bloc/online_checker/online_checker_bloc.dart'; import 'package:enaklo_pos/presentation/home/bloc/online_checker/online_checker_bloc.dart';
import 'package:enaklo_pos/presentation/home/bloc/payment_methods/payment_methods_bloc.dart'; import 'package:enaklo_pos/presentation/home/bloc/payment_methods/payment_methods_bloc.dart';
import 'package:enaklo_pos/presentation/home/bloc/qris/qris_bloc.dart'; import 'package:enaklo_pos/presentation/home/bloc/qris/qris_bloc.dart';
import 'package:enaklo_pos/presentation/home/bloc/status_table/status_table_bloc.dart';
import 'package:enaklo_pos/presentation/home/bloc/checkout/checkout_bloc.dart'; import 'package:enaklo_pos/presentation/home/bloc/checkout/checkout_bloc.dart';
import 'package:enaklo_pos/presentation/report/blocs/item_sales_report/item_sales_report_bloc.dart'; import 'package:enaklo_pos/presentation/report/blocs/item_sales_report/item_sales_report_bloc.dart';
import 'package:enaklo_pos/presentation/report/blocs/payment_method_report/payment_method_report_bloc.dart'; import 'package:enaklo_pos/presentation/report/blocs/payment_method_report/payment_method_report_bloc.dart';
import 'package:enaklo_pos/presentation/report/blocs/product_sales/product_sales_bloc.dart'; import 'package:enaklo_pos/presentation/report/blocs/product_sales/product_sales_bloc.dart';
import 'package:enaklo_pos/presentation/report/blocs/summary/summary_bloc.dart'; import 'package:enaklo_pos/presentation/report/blocs/summary/summary_bloc.dart';
import 'package:enaklo_pos/presentation/sales/blocs/bloc/last_order_table_bloc.dart';
import 'package:enaklo_pos/presentation/sales/blocs/day_sales/day_sales_bloc.dart';
import 'package:enaklo_pos/presentation/setting/bloc/add_product/add_product_bloc.dart'; import 'package:enaklo_pos/presentation/setting/bloc/add_product/add_product_bloc.dart';
import 'package:enaklo_pos/presentation/setting/bloc/create_printer/create_printer_bloc.dart'; import 'package:enaklo_pos/presentation/setting/bloc/create_printer/create_printer_bloc.dart';
import 'package:enaklo_pos/presentation/setting/bloc/get_categories/get_categories_bloc.dart'; import 'package:enaklo_pos/presentation/setting/bloc/get_categories/get_categories_bloc.dart';
@@ -60,18 +65,14 @@ import 'package:enaklo_pos/presentation/setting/bloc/update_printer/update_print
import 'package:enaklo_pos/presentation/table/blocs/change_position_table/change_position_table_bloc.dart'; import 'package:enaklo_pos/presentation/table/blocs/change_position_table/change_position_table_bloc.dart';
import 'package:enaklo_pos/presentation/table/blocs/create_table/create_table_bloc.dart'; import 'package:enaklo_pos/presentation/table/blocs/create_table/create_table_bloc.dart';
import 'package:enaklo_pos/presentation/table/blocs/get_table/get_table_bloc.dart'; import 'package:enaklo_pos/presentation/table/blocs/get_table/get_table_bloc.dart';
import 'package:enaklo_pos/presentation/home/bloc/local_product/local_product_bloc.dart';
import 'package:enaklo_pos/presentation/home/bloc/order/order_bloc.dart'; import 'package:enaklo_pos/presentation/home/bloc/order/order_bloc.dart';
import 'package:enaklo_pos/presentation/report/blocs/transaction_report/transaction_report_bloc.dart'; import 'package:enaklo_pos/presentation/report/blocs/transaction_report/transaction_report_bloc.dart';
import 'package:enaklo_pos/presentation/setting/bloc/add_discount/add_discount_bloc.dart'; import 'package:enaklo_pos/presentation/setting/bloc/add_discount/add_discount_bloc.dart';
import 'package:enaklo_pos/presentation/setting/bloc/discount/discount_bloc.dart'; import 'package:enaklo_pos/presentation/setting/bloc/discount/discount_bloc.dart';
import 'package:enaklo_pos/presentation/setting/bloc/sync_order/sync_order_bloc.dart';
import 'package:enaklo_pos/presentation/setting/bloc/sync_product/sync_product_bloc.dart'; import 'package:enaklo_pos/presentation/setting/bloc/sync_product/sync_product_bloc.dart';
import 'package:enaklo_pos/presentation/setting/bloc/tax_settings/tax_settings_bloc.dart'; import 'package:enaklo_pos/presentation/setting/bloc/tax_settings/tax_settings_bloc.dart';
import 'package:enaklo_pos/presentation/table/blocs/update_table/update_table_bloc.dart'; import 'package:enaklo_pos/presentation/table/blocs/update_table/update_table_bloc.dart';
import 'package:enaklo_pos/presentation/home/bloc/add_order_items/add_order_items_bloc.dart'; import 'package:enaklo_pos/presentation/home/bloc/add_order_items/add_order_items_bloc.dart';
import 'package:flutter/services.dart';
import 'presentation/auth/bloc/login/login_bloc.dart'; import 'presentation/auth/bloc/login/login_bloc.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@@ -79,16 +80,28 @@ import 'presentation/home/pages/dashboard_page.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() async { void main() async {
runZonedGuarded(() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeLeft, await Firebase.initializeApp();
DeviceOrientation.landscapeRight, await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(true);
]);
// await LamanPrint.init(); // ✅ Tambah custom logs
// final dir = await getApplicationDocumentsDirectory(); FlutterError.onError = (errorDetails) {
// Hive.init(dir.path); FirebaseCrashlytics.instance.recordFlutterFatalError(errorDetails);
// Hive.registerAdapter(TableDataAdapter()); };
// ✅ Catch platform errors juga
PlatformDispatcher.instance.onError = (error, stack) {
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
return true;
};
runApp(ProviderScope(child: MyApp())); runApp(ProviderScope(child: MyApp()));
}, (error, stack) {
// ✅ Ini udah bener
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
});
} }
class MyApp extends StatefulWidget { class MyApp extends StatefulWidget {
@@ -129,10 +142,6 @@ class _MyAppState extends State<MyApp> {
BlocProvider( BlocProvider(
create: (context) => SyncProductBloc(ProductRemoteDatasource()), create: (context) => SyncProductBloc(ProductRemoteDatasource()),
), ),
BlocProvider(
create: (context) =>
LocalProductBloc(ProductLocalDatasource.instance),
),
BlocProvider( BlocProvider(
create: (context) => create: (context) =>
CheckoutBloc(settingsLocalDatasource: SettingsLocalDatasource()), CheckoutBloc(settingsLocalDatasource: SettingsLocalDatasource()),
@@ -146,9 +155,6 @@ class _MyAppState extends State<MyApp> {
return OrderBloc(OrderRemoteDatasource()); return OrderBloc(OrderRemoteDatasource());
}, },
), ),
BlocProvider(
create: (context) => SyncOrderBloc(OrderRemoteDatasource()),
),
BlocProvider( BlocProvider(
create: (context) => DiscountBloc(DiscountRemoteDatasource()), create: (context) => DiscountBloc(DiscountRemoteDatasource()),
), ),
@@ -170,13 +176,6 @@ class _MyAppState extends State<MyApp> {
BlocProvider( BlocProvider(
create: (context) => UpdateTableBloc(), create: (context) => UpdateTableBloc(),
), ),
BlocProvider(
create: (context) => StatusTableBloc(ProductLocalDatasource.instance),
),
BlocProvider(
create: (context) =>
LastOrderTableBloc(ProductLocalDatasource.instance),
),
BlocProvider( BlocProvider(
create: (context) => GetTableStatusBloc(TableRemoteDataSource()), create: (context) => GetTableStatusBloc(TableRemoteDataSource()),
), ),
@@ -208,9 +207,6 @@ class _MyAppState extends State<MyApp> {
create: (context) => create: (context) =>
PaymentMethodReportBloc(AnalyticRemoteDatasource()), PaymentMethodReportBloc(AnalyticRemoteDatasource()),
), ),
BlocProvider(
create: (context) => DaySalesBloc(ProductLocalDatasource.instance),
),
BlocProvider( BlocProvider(
create: (context) => QrisBloc(MidtransRemoteDatasource()), create: (context) => QrisBloc(MidtransRemoteDatasource()),
), ),
@@ -243,7 +239,7 @@ class _MyAppState extends State<MyApp> {
create: (context) => AddOrderItemsBloc(OrderRemoteDatasource()), create: (context) => AddOrderItemsBloc(OrderRemoteDatasource()),
), ),
BlocProvider( BlocProvider(
create: (context) => ProductLoaderBloc(ProductRemoteDatasource()), create: (context) => ProductLoaderBloc(),
), ),
BlocProvider( BlocProvider(
create: (context) => OrderFormBloc(OrderRemoteDatasource()), create: (context) => OrderFormBloc(OrderRemoteDatasource()),
@@ -279,11 +275,26 @@ class _MyAppState extends State<MyApp> {
create: (context) => UploadFileBloc(FileRemoteDataSource()), create: (context) => UploadFileBloc(FileRemoteDataSource()),
), ),
BlocProvider( BlocProvider(
create: (context) => CategoryLoaderBloc(CategoryRemoteDatasource()), create: (context) => CategoryLoaderBloc(),
), ),
BlocProvider( BlocProvider(
create: (context) => GetPrinterTicketBloc(), create: (context) => GetPrinterTicketBloc(),
), ),
BlocProvider(
create: (context) => TransferTableBloc(TableRemoteDataSource()),
),
BlocProvider(
create: (context) => ReportBloc(AnalyticRemoteDatasource()),
),
BlocProvider(
create: (context) => InventoryReportBloc(AnalyticRemoteDatasource()),
),
BlocProvider(
create: (context) => CategoryReportBloc(AnalyticRemoteDatasource()),
),
BlocProvider(
create: (context) => DataSyncBloc(),
),
], ],
child: MaterialApp( child: MaterialApp(
navigatorKey: AuthInterceptor.navigatorKey, navigatorKey: AuthInterceptor.navigatorKey,
+2 -2
View File
@@ -1,3 +1,4 @@
import 'package:enaklo_pos/presentation/data_sync/pages/data_sync_page.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:enaklo_pos/data/datasources/auth_local_datasource.dart'; import 'package:enaklo_pos/data/datasources/auth_local_datasource.dart';
@@ -9,7 +10,6 @@ import '../../core/components/buttons.dart';
import '../../core/components/custom_text_field.dart'; import '../../core/components/custom_text_field.dart';
import '../../core/components/spaces.dart'; import '../../core/components/spaces.dart';
import '../../core/constants/colors.dart'; import '../../core/constants/colors.dart';
import '../home/pages/dashboard_page.dart';
import 'bloc/login/login_bloc.dart'; import 'bloc/login/login_bloc.dart';
class LoginPage extends StatefulWidget { class LoginPage extends StatefulWidget {
@@ -104,7 +104,7 @@ class _LoginPageState extends State<LoginPage> {
Navigator.pushReplacement( Navigator.pushReplacement(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => const DashboardPage(), builder: (context) => const DataSyncPage(),
), ),
); );
}, },
@@ -0,0 +1,232 @@
import 'dart:async';
import 'dart:developer';
import 'package:bloc/bloc.dart';
import 'package:enaklo_pos/data/datasources/product/product_local_datasource.dart';
import 'package:enaklo_pos/data/datasources/category/category_local_datasource.dart';
import 'package:enaklo_pos/data/repositories/category/category_repository.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../../data/datasources/product_remote_datasource.dart';
part 'data_sync_event.dart';
part 'data_sync_state.dart';
part 'data_sync_bloc.freezed.dart';
enum SyncStep { categories, products, variants, completed }
class SyncStats {
final int totalProducts;
final int totalCategories;
final int totalVariants;
final double databaseSizeMB;
SyncStats({
required this.totalProducts,
required this.totalCategories,
required this.totalVariants,
required this.databaseSizeMB,
});
}
class DataSyncBloc extends Bloc<DataSyncEvent, DataSyncState> {
final ProductRemoteDatasource _productRemoteDatasource =
ProductRemoteDatasource();
final ProductLocalDatasource _productLocalDatasource =
ProductLocalDatasource.instance;
final CategoryLocalDatasource _categoryLocalDatasource =
CategoryLocalDatasource.instance;
final CategoryRepository _categoryRepository = CategoryRepository.instance;
Timer? _progressTimer;
bool _isCancelled = false;
DataSyncBloc() : super(const DataSyncState.initial()) {
on<_StartSync>(_onStartSync);
on<_CancelSync>(_onCancelSync);
}
@override
Future<void> close() {
_progressTimer?.cancel();
return super.close();
}
Future<void> _onStartSync(
_StartSync event,
Emitter<DataSyncState> emit,
) async {
log('🔄 Starting full data sync (categories + products)...');
_isCancelled = false;
try {
// Step 1: Clear existing local data
emit(const DataSyncState.syncing(
SyncStep.categories, 0.05, 'Membersihkan data lama...'));
await _productLocalDatasource.clearAllProducts();
await _categoryLocalDatasource.clearAllCategories();
if (_isCancelled) return;
// Step 2: Sync categories first (products depend on categories)
await _syncCategories(emit);
if (_isCancelled) return;
// Step 3: Sync products
await _syncProducts(emit);
if (_isCancelled) return;
// Step 4: Generate final stats
emit(const DataSyncState.syncing(
SyncStep.completed, 0.95, 'Menyelesaikan sinkronisasi...'));
final stats = await _generateSyncStats();
emit(DataSyncState.completed(stats));
log('✅ Full sync completed successfully');
} catch (e) {
log('❌ Sync failed: $e');
emit(DataSyncState.error('Gagal sinkronisasi: $e'));
}
}
Future<void> _syncCategories(Emitter<DataSyncState> emit) async {
log('📁 Syncing categories...');
emit(const DataSyncState.syncing(
SyncStep.categories,
0.1,
'Mengunduh kategori...',
));
try {
// Use CategoryRepository sync method
final result = await _categoryRepository.syncAllCategories();
await result.fold(
(failure) async {
throw Exception('Gagal sync kategori: $failure');
},
(successMessage) async {
log('✅ Categories sync completed: $successMessage');
emit(const DataSyncState.syncing(
SyncStep.categories,
0.2,
'Kategori berhasil diunduh',
));
},
);
} catch (e) {
log('❌ Category sync failed: $e');
throw Exception('Gagal sync kategori: $e');
}
}
Future<void> _syncProducts(Emitter<DataSyncState> emit) async {
log('📦 Syncing products...');
int page = 1;
int totalSynced = 0;
int? totalCount;
int? totalPages;
bool shouldContinue = true;
while (!_isCancelled && shouldContinue) {
// Calculate accurate progress (categories = 0.2, products = 0.2-0.9)
double progress = 0.2;
if (totalCount != null && (totalCount ?? 0) > 0) {
progress = 0.2 + (totalSynced / (totalCount ?? 0)) * 0.7;
}
emit(DataSyncState.syncing(
SyncStep.products,
progress,
totalCount != null
? 'Mengunduh produk... ($totalSynced dari $totalCount)'
: 'Mengunduh produk... ($totalSynced produk)',
));
final result = await _productRemoteDatasource.getProducts(
page: page,
limit: 50, // Bigger batch for sync
);
await result.fold(
(failure) async {
throw Exception(failure);
},
(response) async {
final products = response.data?.products ?? [];
final responseData = response.data;
// Get pagination info from first response
if (page == 1 && responseData != null) {
totalCount = responseData.totalCount;
totalPages = responseData.totalPages;
log('📊 Total products to sync: $totalCount (${totalPages} pages)');
}
if (products.isEmpty) {
shouldContinue = false;
return;
}
// Save to local database in batches
await _productLocalDatasource.saveProductsBatch(products);
totalSynced += products.length;
page++;
log('📦 Synced page ${page - 1}: ${products.length} products (Total: $totalSynced)');
// Check if we reached the end using pagination info
if (totalPages != null && page > (totalPages ?? 0)) {
shouldContinue = false;
return;
}
// Fallback check if pagination info not available
if (products.length < 50) {
shouldContinue = false;
return;
}
// Small delay to prevent overwhelming the server
await Future.delayed(Duration(milliseconds: 100));
},
);
}
emit(DataSyncState.syncing(
SyncStep.products,
0.9,
'Produk berhasil diunduh ($totalSynced dari ${totalCount ?? totalSynced})',
));
log('✅ Products sync completed: $totalSynced products synced');
}
Future<SyncStats> _generateSyncStats() async {
final productStats = await _productLocalDatasource.getDatabaseStats();
final categoryStats = await _categoryLocalDatasource.getDatabaseStats();
return SyncStats(
totalProducts: productStats['total_products'] ?? 0,
totalCategories: categoryStats['total_categories'] ?? 0,
totalVariants: productStats['total_variants'] ?? 0,
databaseSizeMB: (productStats['database_size_mb'] ?? 0.0) +
(categoryStats['database_size_mb'] ?? 0.0),
);
}
Future<void> _onCancelSync(
_CancelSync event,
Emitter<DataSyncState> emit,
) async {
log('⏹️ Cancelling sync...');
_isCancelled = true;
_progressTimer?.cancel();
emit(const DataSyncState.initial());
}
}
@@ -0,0 +1,962 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'data_sync_bloc.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
/// @nodoc
mixin _$DataSyncEvent {
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() startSync,
required TResult Function() cancelSync,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? startSync,
TResult? Function()? cancelSync,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? startSync,
TResult Function()? cancelSync,
required TResult orElse(),
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_StartSync value) startSync,
required TResult Function(_CancelSync value) cancelSync,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_StartSync value)? startSync,
TResult? Function(_CancelSync value)? cancelSync,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_StartSync value)? startSync,
TResult Function(_CancelSync value)? cancelSync,
required TResult orElse(),
}) =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $DataSyncEventCopyWith<$Res> {
factory $DataSyncEventCopyWith(
DataSyncEvent value, $Res Function(DataSyncEvent) then) =
_$DataSyncEventCopyWithImpl<$Res, DataSyncEvent>;
}
/// @nodoc
class _$DataSyncEventCopyWithImpl<$Res, $Val extends DataSyncEvent>
implements $DataSyncEventCopyWith<$Res> {
_$DataSyncEventCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of DataSyncEvent
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
abstract class _$$StartSyncImplCopyWith<$Res> {
factory _$$StartSyncImplCopyWith(
_$StartSyncImpl value, $Res Function(_$StartSyncImpl) then) =
__$$StartSyncImplCopyWithImpl<$Res>;
}
/// @nodoc
class __$$StartSyncImplCopyWithImpl<$Res>
extends _$DataSyncEventCopyWithImpl<$Res, _$StartSyncImpl>
implements _$$StartSyncImplCopyWith<$Res> {
__$$StartSyncImplCopyWithImpl(
_$StartSyncImpl _value, $Res Function(_$StartSyncImpl) _then)
: super(_value, _then);
/// Create a copy of DataSyncEvent
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
class _$StartSyncImpl implements _StartSync {
const _$StartSyncImpl();
@override
String toString() {
return 'DataSyncEvent.startSync()';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType && other is _$StartSyncImpl);
}
@override
int get hashCode => runtimeType.hashCode;
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() startSync,
required TResult Function() cancelSync,
}) {
return startSync();
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? startSync,
TResult? Function()? cancelSync,
}) {
return startSync?.call();
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? startSync,
TResult Function()? cancelSync,
required TResult orElse(),
}) {
if (startSync != null) {
return startSync();
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_StartSync value) startSync,
required TResult Function(_CancelSync value) cancelSync,
}) {
return startSync(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_StartSync value)? startSync,
TResult? Function(_CancelSync value)? cancelSync,
}) {
return startSync?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_StartSync value)? startSync,
TResult Function(_CancelSync value)? cancelSync,
required TResult orElse(),
}) {
if (startSync != null) {
return startSync(this);
}
return orElse();
}
}
abstract class _StartSync implements DataSyncEvent {
const factory _StartSync() = _$StartSyncImpl;
}
/// @nodoc
abstract class _$$CancelSyncImplCopyWith<$Res> {
factory _$$CancelSyncImplCopyWith(
_$CancelSyncImpl value, $Res Function(_$CancelSyncImpl) then) =
__$$CancelSyncImplCopyWithImpl<$Res>;
}
/// @nodoc
class __$$CancelSyncImplCopyWithImpl<$Res>
extends _$DataSyncEventCopyWithImpl<$Res, _$CancelSyncImpl>
implements _$$CancelSyncImplCopyWith<$Res> {
__$$CancelSyncImplCopyWithImpl(
_$CancelSyncImpl _value, $Res Function(_$CancelSyncImpl) _then)
: super(_value, _then);
/// Create a copy of DataSyncEvent
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
class _$CancelSyncImpl implements _CancelSync {
const _$CancelSyncImpl();
@override
String toString() {
return 'DataSyncEvent.cancelSync()';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType && other is _$CancelSyncImpl);
}
@override
int get hashCode => runtimeType.hashCode;
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() startSync,
required TResult Function() cancelSync,
}) {
return cancelSync();
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? startSync,
TResult? Function()? cancelSync,
}) {
return cancelSync?.call();
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? startSync,
TResult Function()? cancelSync,
required TResult orElse(),
}) {
if (cancelSync != null) {
return cancelSync();
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_StartSync value) startSync,
required TResult Function(_CancelSync value) cancelSync,
}) {
return cancelSync(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_StartSync value)? startSync,
TResult? Function(_CancelSync value)? cancelSync,
}) {
return cancelSync?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_StartSync value)? startSync,
TResult Function(_CancelSync value)? cancelSync,
required TResult orElse(),
}) {
if (cancelSync != null) {
return cancelSync(this);
}
return orElse();
}
}
abstract class _CancelSync implements DataSyncEvent {
const factory _CancelSync() = _$CancelSyncImpl;
}
/// @nodoc
mixin _$DataSyncState {
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() initial,
required TResult Function(SyncStep step, double progress, String message)
syncing,
required TResult Function(SyncStats stats) completed,
required TResult Function(String message) error,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function(SyncStep step, double progress, String message)? syncing,
TResult? Function(SyncStats stats)? completed,
TResult? Function(String message)? error,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function(SyncStep step, double progress, String message)? syncing,
TResult Function(SyncStats stats)? completed,
TResult Function(String message)? error,
required TResult orElse(),
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_Initial value) initial,
required TResult Function(_Syncing value) syncing,
required TResult Function(_Completed value) completed,
required TResult Function(_Error value) error,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_Initial value)? initial,
TResult? Function(_Syncing value)? syncing,
TResult? Function(_Completed value)? completed,
TResult? Function(_Error value)? error,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_Initial value)? initial,
TResult Function(_Syncing value)? syncing,
TResult Function(_Completed value)? completed,
TResult Function(_Error value)? error,
required TResult orElse(),
}) =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $DataSyncStateCopyWith<$Res> {
factory $DataSyncStateCopyWith(
DataSyncState value, $Res Function(DataSyncState) then) =
_$DataSyncStateCopyWithImpl<$Res, DataSyncState>;
}
/// @nodoc
class _$DataSyncStateCopyWithImpl<$Res, $Val extends DataSyncState>
implements $DataSyncStateCopyWith<$Res> {
_$DataSyncStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of DataSyncState
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
abstract class _$$InitialImplCopyWith<$Res> {
factory _$$InitialImplCopyWith(
_$InitialImpl value, $Res Function(_$InitialImpl) then) =
__$$InitialImplCopyWithImpl<$Res>;
}
/// @nodoc
class __$$InitialImplCopyWithImpl<$Res>
extends _$DataSyncStateCopyWithImpl<$Res, _$InitialImpl>
implements _$$InitialImplCopyWith<$Res> {
__$$InitialImplCopyWithImpl(
_$InitialImpl _value, $Res Function(_$InitialImpl) _then)
: super(_value, _then);
/// Create a copy of DataSyncState
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
class _$InitialImpl implements _Initial {
const _$InitialImpl();
@override
String toString() {
return 'DataSyncState.initial()';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType && other is _$InitialImpl);
}
@override
int get hashCode => runtimeType.hashCode;
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() initial,
required TResult Function(SyncStep step, double progress, String message)
syncing,
required TResult Function(SyncStats stats) completed,
required TResult Function(String message) error,
}) {
return initial();
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function(SyncStep step, double progress, String message)? syncing,
TResult? Function(SyncStats stats)? completed,
TResult? Function(String message)? error,
}) {
return initial?.call();
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function(SyncStep step, double progress, String message)? syncing,
TResult Function(SyncStats stats)? completed,
TResult Function(String message)? error,
required TResult orElse(),
}) {
if (initial != null) {
return initial();
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_Initial value) initial,
required TResult Function(_Syncing value) syncing,
required TResult Function(_Completed value) completed,
required TResult Function(_Error value) error,
}) {
return initial(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_Initial value)? initial,
TResult? Function(_Syncing value)? syncing,
TResult? Function(_Completed value)? completed,
TResult? Function(_Error value)? error,
}) {
return initial?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_Initial value)? initial,
TResult Function(_Syncing value)? syncing,
TResult Function(_Completed value)? completed,
TResult Function(_Error value)? error,
required TResult orElse(),
}) {
if (initial != null) {
return initial(this);
}
return orElse();
}
}
abstract class _Initial implements DataSyncState {
const factory _Initial() = _$InitialImpl;
}
/// @nodoc
abstract class _$$SyncingImplCopyWith<$Res> {
factory _$$SyncingImplCopyWith(
_$SyncingImpl value, $Res Function(_$SyncingImpl) then) =
__$$SyncingImplCopyWithImpl<$Res>;
@useResult
$Res call({SyncStep step, double progress, String message});
}
/// @nodoc
class __$$SyncingImplCopyWithImpl<$Res>
extends _$DataSyncStateCopyWithImpl<$Res, _$SyncingImpl>
implements _$$SyncingImplCopyWith<$Res> {
__$$SyncingImplCopyWithImpl(
_$SyncingImpl _value, $Res Function(_$SyncingImpl) _then)
: super(_value, _then);
/// Create a copy of DataSyncState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? step = null,
Object? progress = null,
Object? message = null,
}) {
return _then(_$SyncingImpl(
null == step
? _value.step
: step // ignore: cast_nullable_to_non_nullable
as SyncStep,
null == progress
? _value.progress
: progress // ignore: cast_nullable_to_non_nullable
as double,
null == message
? _value.message
: message // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
class _$SyncingImpl implements _Syncing {
const _$SyncingImpl(this.step, this.progress, this.message);
@override
final SyncStep step;
@override
final double progress;
@override
final String message;
@override
String toString() {
return 'DataSyncState.syncing(step: $step, progress: $progress, message: $message)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$SyncingImpl &&
(identical(other.step, step) || other.step == step) &&
(identical(other.progress, progress) ||
other.progress == progress) &&
(identical(other.message, message) || other.message == message));
}
@override
int get hashCode => Object.hash(runtimeType, step, progress, message);
/// Create a copy of DataSyncState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$SyncingImplCopyWith<_$SyncingImpl> get copyWith =>
__$$SyncingImplCopyWithImpl<_$SyncingImpl>(this, _$identity);
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() initial,
required TResult Function(SyncStep step, double progress, String message)
syncing,
required TResult Function(SyncStats stats) completed,
required TResult Function(String message) error,
}) {
return syncing(step, progress, message);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function(SyncStep step, double progress, String message)? syncing,
TResult? Function(SyncStats stats)? completed,
TResult? Function(String message)? error,
}) {
return syncing?.call(step, progress, message);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function(SyncStep step, double progress, String message)? syncing,
TResult Function(SyncStats stats)? completed,
TResult Function(String message)? error,
required TResult orElse(),
}) {
if (syncing != null) {
return syncing(step, progress, message);
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_Initial value) initial,
required TResult Function(_Syncing value) syncing,
required TResult Function(_Completed value) completed,
required TResult Function(_Error value) error,
}) {
return syncing(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_Initial value)? initial,
TResult? Function(_Syncing value)? syncing,
TResult? Function(_Completed value)? completed,
TResult? Function(_Error value)? error,
}) {
return syncing?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_Initial value)? initial,
TResult Function(_Syncing value)? syncing,
TResult Function(_Completed value)? completed,
TResult Function(_Error value)? error,
required TResult orElse(),
}) {
if (syncing != null) {
return syncing(this);
}
return orElse();
}
}
abstract class _Syncing implements DataSyncState {
const factory _Syncing(
final SyncStep step, final double progress, final String message) =
_$SyncingImpl;
SyncStep get step;
double get progress;
String get message;
/// Create a copy of DataSyncState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
_$$SyncingImplCopyWith<_$SyncingImpl> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class _$$CompletedImplCopyWith<$Res> {
factory _$$CompletedImplCopyWith(
_$CompletedImpl value, $Res Function(_$CompletedImpl) then) =
__$$CompletedImplCopyWithImpl<$Res>;
@useResult
$Res call({SyncStats stats});
}
/// @nodoc
class __$$CompletedImplCopyWithImpl<$Res>
extends _$DataSyncStateCopyWithImpl<$Res, _$CompletedImpl>
implements _$$CompletedImplCopyWith<$Res> {
__$$CompletedImplCopyWithImpl(
_$CompletedImpl _value, $Res Function(_$CompletedImpl) _then)
: super(_value, _then);
/// Create a copy of DataSyncState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? stats = null,
}) {
return _then(_$CompletedImpl(
null == stats
? _value.stats
: stats // ignore: cast_nullable_to_non_nullable
as SyncStats,
));
}
}
/// @nodoc
class _$CompletedImpl implements _Completed {
const _$CompletedImpl(this.stats);
@override
final SyncStats stats;
@override
String toString() {
return 'DataSyncState.completed(stats: $stats)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$CompletedImpl &&
(identical(other.stats, stats) || other.stats == stats));
}
@override
int get hashCode => Object.hash(runtimeType, stats);
/// Create a copy of DataSyncState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$CompletedImplCopyWith<_$CompletedImpl> get copyWith =>
__$$CompletedImplCopyWithImpl<_$CompletedImpl>(this, _$identity);
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() initial,
required TResult Function(SyncStep step, double progress, String message)
syncing,
required TResult Function(SyncStats stats) completed,
required TResult Function(String message) error,
}) {
return completed(stats);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function(SyncStep step, double progress, String message)? syncing,
TResult? Function(SyncStats stats)? completed,
TResult? Function(String message)? error,
}) {
return completed?.call(stats);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function(SyncStep step, double progress, String message)? syncing,
TResult Function(SyncStats stats)? completed,
TResult Function(String message)? error,
required TResult orElse(),
}) {
if (completed != null) {
return completed(stats);
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_Initial value) initial,
required TResult Function(_Syncing value) syncing,
required TResult Function(_Completed value) completed,
required TResult Function(_Error value) error,
}) {
return completed(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_Initial value)? initial,
TResult? Function(_Syncing value)? syncing,
TResult? Function(_Completed value)? completed,
TResult? Function(_Error value)? error,
}) {
return completed?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_Initial value)? initial,
TResult Function(_Syncing value)? syncing,
TResult Function(_Completed value)? completed,
TResult Function(_Error value)? error,
required TResult orElse(),
}) {
if (completed != null) {
return completed(this);
}
return orElse();
}
}
abstract class _Completed implements DataSyncState {
const factory _Completed(final SyncStats stats) = _$CompletedImpl;
SyncStats get stats;
/// Create a copy of DataSyncState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
_$$CompletedImplCopyWith<_$CompletedImpl> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class _$$ErrorImplCopyWith<$Res> {
factory _$$ErrorImplCopyWith(
_$ErrorImpl value, $Res Function(_$ErrorImpl) then) =
__$$ErrorImplCopyWithImpl<$Res>;
@useResult
$Res call({String message});
}
/// @nodoc
class __$$ErrorImplCopyWithImpl<$Res>
extends _$DataSyncStateCopyWithImpl<$Res, _$ErrorImpl>
implements _$$ErrorImplCopyWith<$Res> {
__$$ErrorImplCopyWithImpl(
_$ErrorImpl _value, $Res Function(_$ErrorImpl) _then)
: super(_value, _then);
/// Create a copy of DataSyncState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? message = null,
}) {
return _then(_$ErrorImpl(
null == message
? _value.message
: message // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
class _$ErrorImpl implements _Error {
const _$ErrorImpl(this.message);
@override
final String message;
@override
String toString() {
return 'DataSyncState.error(message: $message)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ErrorImpl &&
(identical(other.message, message) || other.message == message));
}
@override
int get hashCode => Object.hash(runtimeType, message);
/// Create a copy of DataSyncState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$ErrorImplCopyWith<_$ErrorImpl> get copyWith =>
__$$ErrorImplCopyWithImpl<_$ErrorImpl>(this, _$identity);
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() initial,
required TResult Function(SyncStep step, double progress, String message)
syncing,
required TResult Function(SyncStats stats) completed,
required TResult Function(String message) error,
}) {
return error(message);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function(SyncStep step, double progress, String message)? syncing,
TResult? Function(SyncStats stats)? completed,
TResult? Function(String message)? error,
}) {
return error?.call(message);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function(SyncStep step, double progress, String message)? syncing,
TResult Function(SyncStats stats)? completed,
TResult Function(String message)? error,
required TResult orElse(),
}) {
if (error != null) {
return error(message);
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_Initial value) initial,
required TResult Function(_Syncing value) syncing,
required TResult Function(_Completed value) completed,
required TResult Function(_Error value) error,
}) {
return error(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_Initial value)? initial,
TResult? Function(_Syncing value)? syncing,
TResult? Function(_Completed value)? completed,
TResult? Function(_Error value)? error,
}) {
return error?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_Initial value)? initial,
TResult Function(_Syncing value)? syncing,
TResult Function(_Completed value)? completed,
TResult Function(_Error value)? error,
required TResult orElse(),
}) {
if (error != null) {
return error(this);
}
return orElse();
}
}
abstract class _Error implements DataSyncState {
const factory _Error(final String message) = _$ErrorImpl;
String get message;
/// Create a copy of DataSyncState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
_$$ErrorImplCopyWith<_$ErrorImpl> get copyWith =>
throw _privateConstructorUsedError;
}
@@ -0,0 +1,7 @@
part of 'data_sync_bloc.dart';
@freezed
class DataSyncEvent with _$DataSyncEvent {
const factory DataSyncEvent.startSync() = _StartSync;
const factory DataSyncEvent.cancelSync() = _CancelSync;
}
@@ -0,0 +1,13 @@
part of 'data_sync_bloc.dart';
@freezed
class DataSyncState with _$DataSyncState {
const factory DataSyncState.initial() = _Initial;
const factory DataSyncState.syncing(
SyncStep step,
double progress,
String message,
) = _Syncing;
const factory DataSyncState.completed(SyncStats stats) = _Completed;
const factory DataSyncState.error(String message) = _Error;
}
@@ -0,0 +1,779 @@
import 'dart:async';
import 'package:enaklo_pos/core/extensions/build_context_ext.dart';
import 'package:enaklo_pos/presentation/home/pages/dashboard_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../core/components/buttons.dart';
import '../../../core/constants/colors.dart';
import '../bloc/data_sync_bloc.dart';
class DataSyncPage extends StatefulWidget {
const DataSyncPage({super.key});
@override
State<DataSyncPage> createState() => _DataSyncPageState();
}
class _DataSyncPageState extends State<DataSyncPage>
with TickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _progressAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: Duration(milliseconds: 500),
vsync: this,
);
_progressAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
// Auto start sync
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<DataSyncBloc>().add(const DataSyncEvent.startSync());
});
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final mediaQuery = MediaQuery.of(context);
final isLandscape = mediaQuery.orientation == Orientation.landscape;
final screenHeight = mediaQuery.size.height;
final screenWidth = mediaQuery.size.width;
return Scaffold(
backgroundColor: Colors.grey.shade50,
body: SafeArea(
child: BlocConsumer<DataSyncBloc, DataSyncState>(
listener: (context, state) {
state.maybeWhen(
orElse: () {},
syncing: (step, progress, message) {
_animationController.animateTo(progress);
},
completed: (stats) {
_animationController.animateTo(1.0);
// Navigate to home after delay
Future.delayed(Duration(seconds: 2), () {
context.pushReplacement(DashboardPage());
});
},
error: (message) {
_animationController.stop();
},
);
},
builder: (context, state) {
if (isLandscape) {
return _buildLandscapeLayout(state, screenWidth, screenHeight);
} else {
return _buildPortraitLayout(state, screenHeight);
}
},
),
),
);
}
// Portrait layout
Widget _buildPortraitLayout(DataSyncState state, double screenHeight) {
return Padding(
padding: EdgeInsets.all(24),
child: Column(
children: [
SizedBox(height: screenHeight * 0.08),
_buildHeader(false),
SizedBox(height: screenHeight * 0.08),
Expanded(
child: state.when(
initial: () => _buildInitialState(false),
syncing: (step, progress, message) =>
_buildSyncingState(step, progress, message, false),
completed: (stats) => _buildCompletedState(stats, false),
error: (message) => _buildErrorState(message, false),
),
),
SizedBox(height: 20),
_buildActions(state),
],
),
);
}
// Landscape layout
Widget _buildLandscapeLayout(
DataSyncState state, double screenWidth, double screenHeight) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 32, vertical: 16),
child: Row(
children: [
Expanded(
flex: 2,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildHeader(true),
SizedBox(height: 20),
_buildActions(state),
],
),
),
SizedBox(width: 40),
Expanded(
flex: 3,
child: Container(
height: screenHeight * 0.8,
child: state.when(
initial: () => _buildInitialState(true),
syncing: (step, progress, message) =>
_buildSyncingState(step, progress, message, true),
completed: (stats) => _buildCompletedState(stats, true),
error: (message) => _buildErrorState(message, true),
),
),
),
],
),
);
}
Widget _buildHeader(bool isLandscape) {
return Column(
children: [
Container(
width: isLandscape ? 60 : 80,
height: isLandscape ? 60 : 80,
decoration: BoxDecoration(
color: AppColors.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(isLandscape ? 15 : 20),
),
child: Icon(
Icons.sync,
size: isLandscape ? 30 : 40,
color: AppColors.primary,
),
),
SizedBox(height: isLandscape ? 12 : 20),
Text(
'Sinkronisasi Data',
style: TextStyle(
fontSize: isLandscape ? 20 : 24,
fontWeight: FontWeight.bold,
color: Colors.grey.shade800,
),
),
SizedBox(height: isLandscape ? 4 : 8),
Text(
'Mengunduh kategori dan produk terbaru',
style: TextStyle(
fontSize: isLandscape ? 14 : 16,
color: Colors.grey.shade600,
),
textAlign: TextAlign.center,
),
],
);
}
Widget _buildInitialState(bool isLandscape) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.download_rounded,
size: isLandscape ? 48 : 64,
color: Colors.grey.shade400,
),
SizedBox(height: isLandscape ? 12 : 20),
Text(
'Siap untuk sinkronisasi',
style: TextStyle(
fontSize: isLandscape ? 16 : 18,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: isLandscape ? 4 : 8),
Text(
'Tekan tombol mulai untuk mengunduh data',
style: TextStyle(
fontSize: isLandscape ? 12 : 14,
color: Colors.grey.shade600,
),
textAlign: TextAlign.center,
),
],
);
}
Widget _buildSyncingState(
SyncStep step, double progress, String message, bool isLandscape) {
return SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Progress circle
Stack(
alignment: Alignment.center,
children: [
SizedBox(
width: isLandscape ? 100 : 120,
height: isLandscape ? 100 : 120,
child: AnimatedBuilder(
animation: _progressAnimation,
builder: (context, child) {
return CircularProgressIndicator(
value: _progressAnimation.value,
strokeWidth: isLandscape ? 6 : 8,
backgroundColor: Colors.grey.shade200,
valueColor:
AlwaysStoppedAnimation<Color>(AppColors.primary),
);
},
),
),
Column(
children: [
Icon(
_getSyncIcon(step),
size: isLandscape ? 24 : 32,
color: AppColors.primary,
),
SizedBox(height: 2),
AnimatedBuilder(
animation: _progressAnimation,
builder: (context, child) {
return Text(
'${(_progressAnimation.value * 100).toInt()}%',
style: TextStyle(
fontSize: isLandscape ? 14 : 16,
fontWeight: FontWeight.bold,
color: AppColors.primary,
),
);
},
),
],
),
],
),
SizedBox(height: isLandscape ? 20 : 30),
// Step indicator
_buildStepIndicator(step, isLandscape),
SizedBox(height: isLandscape ? 12 : 20),
// Current message
Container(
padding: EdgeInsets.symmetric(
horizontal: isLandscape ? 16 : 20,
vertical: isLandscape ? 8 : 12),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Text(
message,
style: TextStyle(
color: Colors.blue.shade700,
fontSize: isLandscape ? 12 : 14,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
),
SizedBox(height: isLandscape ? 12 : 20),
// Sync details
_buildSyncDetails(step, progress, isLandscape),
],
),
);
}
Widget _buildStepIndicator(SyncStep currentStep, bool isLandscape) {
final steps = [
('Kategori', SyncStep.categories, Icons.category),
('Produk', SyncStep.products, Icons.inventory_2),
('Variant', SyncStep.variants, Icons.tune),
('Selesai', SyncStep.completed, Icons.check_circle),
];
if (isLandscape) {
// Vertical layout for landscape
return Column(
children: steps.map((stepData) {
final (label, step, icon) = stepData;
final isActive = step == currentStep;
final isCompleted = step.index < currentStep.index;
return Container(
margin: EdgeInsets.symmetric(vertical: 2),
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: isActive
? AppColors.primary.withOpacity(0.1)
: isCompleted
? Colors.green.shade50
: Colors.grey.shade100,
borderRadius: BorderRadius.circular(6),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
isCompleted ? Icons.check : icon,
size: 12,
color: isActive
? AppColors.primary
: isCompleted
? Colors.green.shade600
: Colors.grey.shade500,
),
SizedBox(width: 4),
Text(
label,
style: TextStyle(
fontSize: 10,
fontWeight: isActive ? FontWeight.w600 : FontWeight.normal,
color: isActive
? AppColors.primary
: isCompleted
? Colors.green.shade600
: Colors.grey.shade600,
),
),
],
),
);
}).toList(),
);
} else {
// Horizontal layout for portrait
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: steps.map((stepData) {
final (label, step, icon) = stepData;
final isActive = step == currentStep;
final isCompleted = step.index < currentStep.index;
return Container(
margin: EdgeInsets.symmetric(horizontal: 4),
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: isActive
? AppColors.primary.withOpacity(0.1)
: isCompleted
? Colors.green.shade50
: Colors.grey.shade100,
borderRadius: BorderRadius.circular(6),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
isCompleted ? Icons.check : icon,
size: 14,
color: isActive
? AppColors.primary
: isCompleted
? Colors.green.shade600
: Colors.grey.shade500,
),
SizedBox(width: 4),
Text(
label,
style: TextStyle(
fontSize: 11,
fontWeight: isActive ? FontWeight.w600 : FontWeight.normal,
color: isActive
? AppColors.primary
: isCompleted
? Colors.green.shade600
: Colors.grey.shade600,
),
),
],
),
);
}).toList(),
);
}
}
Widget _buildSyncDetails(SyncStep step, double progress, bool isLandscape) {
return Container(
padding: EdgeInsets.all(isLandscape ? 12 : 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade200),
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Status:',
style: TextStyle(
fontSize: isLandscape ? 12 : 14,
fontWeight: FontWeight.w500,
color: Colors.grey.shade700,
),
),
Text(
_getStepLabel(step),
style: TextStyle(
fontSize: isLandscape ? 12 : 14,
fontWeight: FontWeight.w600,
color: AppColors.primary,
),
),
],
),
SizedBox(height: isLandscape ? 6 : 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Progress:',
style: TextStyle(
fontSize: isLandscape ? 12 : 14,
fontWeight: FontWeight.w500,
color: Colors.grey.shade700,
),
),
Text(
'${(progress * 100).toInt()}%',
style: TextStyle(
fontSize: isLandscape ? 12 : 14,
fontWeight: FontWeight.w600,
color: AppColors.primary,
),
),
],
),
],
),
);
}
Widget _buildCompletedState(SyncStats stats, bool isLandscape) {
return SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Success icon
Container(
width: isLandscape ? 80 : 100,
height: isLandscape ? 80 : 100,
decoration: BoxDecoration(
color: Colors.green.shade50,
borderRadius: BorderRadius.circular(isLandscape ? 40 : 50),
),
child: Icon(
Icons.check_circle,
size: isLandscape ? 48 : 60,
color: Colors.green.shade600,
),
),
SizedBox(height: isLandscape ? 20 : 30),
Text(
'Sinkronisasi Berhasil!',
style: TextStyle(
fontSize: isLandscape ? 18 : 22,
fontWeight: FontWeight.bold,
color: Colors.green.shade700,
),
),
SizedBox(height: isLandscape ? 8 : 16),
Text(
'Data berhasil diunduh ke perangkat',
style: TextStyle(
fontSize: isLandscape ? 14 : 16,
color: Colors.grey.shade600,
),
),
SizedBox(height: isLandscape ? 20 : 30),
// Stats cards
Container(
padding: EdgeInsets.all(isLandscape ? 16 : 20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade200),
),
child: Column(
children: [
Text(
'Data yang Diunduh',
style: TextStyle(
fontSize: isLandscape ? 14 : 16,
fontWeight: FontWeight.w600,
color: Colors.grey.shade700,
),
),
SizedBox(height: isLandscape ? 12 : 16),
if (isLandscape)
// Vertical layout for landscape
Column(
children: [
_buildStatItem('Kategori', '${stats.totalCategories}',
Icons.category, Colors.blue, isLandscape),
SizedBox(height: 8),
_buildStatItem('Produk', '${stats.totalProducts}',
Icons.inventory_2, Colors.green, isLandscape),
SizedBox(height: 8),
_buildStatItem('Variant', '${stats.totalVariants}',
Icons.tune, Colors.orange, isLandscape),
],
)
else
// Horizontal layout for portrait
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStatItem('Kategori', '${stats.totalCategories}',
Icons.category, Colors.blue, isLandscape),
_buildStatItem('Produk', '${stats.totalProducts}',
Icons.inventory_2, Colors.green, isLandscape),
_buildStatItem('Variant', '${stats.totalVariants}',
Icons.tune, Colors.orange, isLandscape),
],
),
],
),
),
SizedBox(height: isLandscape ? 12 : 20),
Container(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(6),
),
child: Text(
'Mengalihkan ke halaman utama...',
style: TextStyle(
color: Colors.grey.shade600,
fontSize: isLandscape ? 10 : 12,
),
),
),
],
),
);
}
Widget _buildErrorState(String message, bool isLandscape) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: isLandscape ? 48 : 64,
color: Colors.red.shade400,
),
SizedBox(height: isLandscape ? 12 : 20),
Text(
'Sinkronisasi Gagal',
style: TextStyle(
fontSize: isLandscape ? 16 : 20,
fontWeight: FontWeight.bold,
color: Colors.red.shade600,
),
),
SizedBox(height: isLandscape ? 8 : 12),
Container(
padding: EdgeInsets.all(isLandscape ? 12 : 16),
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Text(
message,
style: TextStyle(
fontSize: isLandscape ? 12 : 14,
color: Colors.red.shade700,
),
textAlign: TextAlign.center,
),
),
SizedBox(height: isLandscape ? 12 : 20),
Text(
'Periksa koneksi internet dan coba lagi',
style: TextStyle(
fontSize: isLandscape ? 12 : 14,
color: Colors.grey.shade600,
),
textAlign: TextAlign.center,
),
],
);
}
Widget _buildStatItem(String label, String value, IconData icon, Color color,
bool isLandscape) {
if (isLandscape) {
// Horizontal layout for landscape
return Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 20, color: color),
SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
value,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: color,
),
),
Text(
label,
style: TextStyle(
fontSize: 10,
color: Colors.grey.shade600,
),
),
],
),
],
),
);
} else {
// Vertical layout for portrait
return Column(
children: [
Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, size: 24, color: color),
),
SizedBox(height: 8),
Text(
value,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: color,
),
),
Text(
label,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
],
);
}
}
Widget _buildActions(DataSyncState state) {
return state.when(
initial: () => Button.filled(
onPressed: () {
context.read<DataSyncBloc>().add(const DataSyncEvent.startSync());
},
label: 'Mulai Sinkronisasi',
),
syncing: (step, progress, message) => Button.outlined(
onPressed: () {
context.read<DataSyncBloc>().add(const DataSyncEvent.cancelSync());
},
label: 'Batalkan',
),
completed: (stats) => Button.filled(
onPressed: () {
context.pushReplacement(DashboardPage());
},
label: 'Lanjutkan ke Aplikasi',
),
error: (message) => Row(
children: [
Expanded(
child: Button.outlined(
onPressed: () {
context.pushReplacement(DashboardPage());
},
label: 'Lewati',
),
),
SizedBox(width: 16),
Expanded(
child: Button.filled(
onPressed: () {
context
.read<DataSyncBloc>()
.add(const DataSyncEvent.startSync());
},
label: 'Coba Lagi',
),
),
],
),
);
}
IconData _getSyncIcon(SyncStep step) {
switch (step) {
case SyncStep.categories:
return Icons.category;
case SyncStep.products:
return Icons.inventory_2;
case SyncStep.variants:
return Icons.tune;
case SyncStep.completed:
return Icons.check_circle;
}
}
String _getStepLabel(SyncStep step) {
switch (step) {
case SyncStep.categories:
return 'Mengunduh Kategori';
case SyncStep.products:
return 'Mengunduh Produk';
case SyncStep.variants:
return 'Mengunduh Variant';
case SyncStep.completed:
return 'Selesai';
}
}
}
@@ -1,6 +1,8 @@
import 'package:bloc/bloc.dart'; import 'dart:async';
import 'package:enaklo_pos/data/datasources/category_remote_datasource.dart'; import 'dart:developer';
import 'package:enaklo_pos/data/models/response/category_response_model.dart'; import 'package:enaklo_pos/data/models/response/category_response_model.dart';
import 'package:enaklo_pos/data/repositories/category/category_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
part 'category_loader_event.dart'; part 'category_loader_event.dart';
@@ -9,35 +11,314 @@ part 'category_loader_bloc.freezed.dart';
class CategoryLoaderBloc class CategoryLoaderBloc
extends Bloc<CategoryLoaderEvent, CategoryLoaderState> { extends Bloc<CategoryLoaderEvent, CategoryLoaderState> {
final CategoryRemoteDatasource _datasource; final CategoryRepository _categoryRepository = CategoryRepository.instance;
CategoryLoaderBloc(this._datasource) : super(CategoryLoaderState.initial()) {
on<_Get>((event, emit) async { Timer? _searchDebounce;
emit(const _Loading()); bool _isLoadingMore = false;
final result = await _datasource.getCategories(limit: 50);
result.fold( CategoryLoaderBloc() : super(const CategoryLoaderState.initial()) {
(l) => emit(_Error(l)), on<_GetCategories>(_onGetCategories);
(r) async { on<_LoadMore>(_onLoadMore);
List<CategoryModel> categories = r.data.categories; on<_Refresh>(_onRefresh);
categories.insert( on<_Search>(_onSearch);
0, on<_SyncAll>(_onSyncAll);
CategoryModel( on<_GetAllCategories>(_onGetAllCategories);
id: "", on<_ClearCache>(_onClearCache);
name: 'Semua', on<_GetDatabaseStats>(_onGetDatabaseStats);
organizationId: '', }
businessType: '',
metadata: {}, @override
createdAt: DateTime.now(), Future<void> close() {
updatedAt: DateTime.now(), _searchDebounce?.cancel();
), return super.close();
}
// ========================================
// GET CATEGORIES (Remote-first with local fallback)
// ========================================
Future<void> _onGetCategories(
_GetCategories event,
Emitter<CategoryLoaderState> emit,
) async {
emit(const CategoryLoaderState.loading());
_isLoadingMore = false;
log('📱 Loading categories - isActive: ${event.isActive}, forceRemote: ${event.forceRemote}');
final result = await _categoryRepository.getCategories(
page: 1,
limit: 50,
isActive: event.isActive,
search: event.search,
forceRemote: event.forceRemote,
); );
emit(_Loaded(categories, null));
await result.fold(
(failure) async {
log('❌ Error loading categories: $failure');
emit(CategoryLoaderState.error(failure));
},
(response) async {
final categories = [
CategoryModel.all(),
...response.data.categories,
];
final totalPages = response.data.totalPages;
final hasReachedMax = categories.length < 50 || 1 >= totalPages;
log('✅ Categories loaded: ${categories.length}, hasReachedMax: $hasReachedMax');
emit(CategoryLoaderState.loaded(
categories: categories,
hasReachedMax: hasReachedMax,
currentPage: 1,
isLoadingMore: false,
isActive: event.isActive,
searchQuery: event.search,
));
},
);
}
// ========================================
// LOAD MORE CATEGORIES
// ========================================
Future<void> _onLoadMore(
_LoadMore event,
Emitter<CategoryLoaderState> emit,
) async {
final currentState = state;
if (currentState is! _Loaded ||
currentState.hasReachedMax ||
_isLoadingMore ||
currentState.isLoadingMore) {
log('⏹️ Load more blocked - state: ${currentState.runtimeType}, isLoadingMore: $_isLoadingMore');
return;
}
_isLoadingMore = true;
emit(currentState.copyWith(isLoadingMore: true));
final nextPage = currentState.currentPage + 1;
log('📄 Loading more categories - page: $nextPage');
try {
final result = await _categoryRepository.getCategories(
page: nextPage,
limit: 10,
isActive: currentState.isActive,
search: currentState.searchQuery,
);
await result.fold(
(failure) async {
log('❌ Error loading more categories: $failure');
emit(currentState.copyWith(isLoadingMore: false));
},
(response) async {
final newCategories = response.data.categories;
final totalPages = response.data.totalPages;
// Prevent duplicate categories
final currentCategoryIds =
currentState.categories.map((c) => c.id).toSet();
final filteredNewCategories = newCategories
.where((category) => !currentCategoryIds.contains(category.id))
.toList();
final allCategories =
List<CategoryModel>.from(currentState.categories)
..addAll(filteredNewCategories);
final hasReachedMax =
newCategories.length < 10 || nextPage >= totalPages;
log('✅ More categories loaded: ${filteredNewCategories.length} new, total: ${allCategories.length}');
emit(CategoryLoaderState.loaded(
categories: allCategories,
hasReachedMax: hasReachedMax,
currentPage: nextPage,
isLoadingMore: false,
isActive: currentState.isActive,
searchQuery: currentState.searchQuery,
));
},
);
} catch (e) {
log('❌ Exception loading more categories: $e');
emit(currentState.copyWith(isLoadingMore: false));
} finally {
_isLoadingMore = false;
}
}
// ========================================
// REFRESH CATEGORIES
// ========================================
Future<void> _onRefresh(
_Refresh event,
Emitter<CategoryLoaderState> emit,
) async {
final currentState = state;
bool isActive = true;
String? searchQuery;
if (currentState is _Loaded) {
isActive = currentState.isActive;
searchQuery = currentState.searchQuery;
}
_isLoadingMore = false;
_searchDebounce?.cancel();
log('🔄 Refreshing categories');
// Clear local cache
_categoryRepository.clearCache();
add(CategoryLoaderEvent.getCategories(
isActive: isActive,
search: searchQuery,
forceRemote: true, // Force remote refresh
));
}
// ========================================
// SEARCH CATEGORIES
// ========================================
Future<void> _onSearch(
_Search event,
Emitter<CategoryLoaderState> emit,
) async {
// Cancel previous search
_searchDebounce?.cancel();
// Debounce search for better UX
_searchDebounce = Timer(Duration(milliseconds: 300), () async {
emit(const CategoryLoaderState.loading());
_isLoadingMore = false;
log('🔍 Searching categories: "${event.query}"');
final result = await _categoryRepository.getCategories(
page: 1,
limit: 20, // More results for search
isActive: event.isActive,
search: event.query,
);
await result.fold(
(failure) async {
log('❌ Search error: $failure');
emit(CategoryLoaderState.error(failure));
},
(response) async {
final categories = response.data.categories;
final totalPages = response.data.totalPages;
final hasReachedMax = categories.length < 20 || 1 >= totalPages;
log('✅ Search results: ${categories.length} categories found');
emit(CategoryLoaderState.loaded(
categories: categories,
hasReachedMax: hasReachedMax,
currentPage: 1,
isLoadingMore: false,
isActive: event.isActive,
searchQuery: event.query,
));
}, },
); );
}); });
on<_SetCategoryId>((event, emit) async { }
var currentState = state as _Loaded;
emit(_Loaded(currentState.categories, event.categoryId)); // ========================================
// SYNC ALL CATEGORIES
// ========================================
Future<void> _onSyncAll(
_SyncAll event,
Emitter<CategoryLoaderState> emit,
) async {
emit(const CategoryLoaderState.syncing());
log('🔄 Starting full category sync...');
final result = await _categoryRepository.syncAllCategories();
await result.fold(
(failure) async {
log('❌ Sync failed: $failure');
emit(CategoryLoaderState.syncError(failure));
// After sync error, try to load local data
Timer(Duration(seconds: 2), () {
add(const CategoryLoaderEvent.getCategories());
}); });
},
(successMessage) async {
log('✅ Sync completed: $successMessage');
emit(CategoryLoaderState.syncSuccess(successMessage));
// After successful sync, load the updated data
Timer(Duration(seconds: 1), () {
add(const CategoryLoaderEvent.getCategories());
});
},
);
}
// ========================================
// GET ALL CATEGORIES (For Dropdown)
// ========================================
Future<void> _onGetAllCategories(
_GetAllCategories event,
Emitter<CategoryLoaderState> emit,
) async {
try {
log('📋 Loading all categories for dropdown...');
final categories = await _categoryRepository.getAllCategories();
emit(CategoryLoaderState.allCategoriesLoaded(categories));
log('✅ All categories loaded: ${categories.length}');
} catch (e) {
log('❌ Error loading all categories: $e');
emit(CategoryLoaderState.error('Gagal memuat semua kategori: $e'));
}
}
// ========================================
// GET DATABASE STATS
// ========================================
Future<void> _onGetDatabaseStats(
_GetDatabaseStats event,
Emitter<CategoryLoaderState> emit,
) async {
try {
final stats = await _categoryRepository.getDatabaseStats();
log('📊 Category database stats retrieved: $stats');
// You can emit a special state here if needed for UI updates
// For now, just log the stats
} catch (e) {
log('❌ Error getting category database stats: $e');
}
}
// ========================================
// CLEAR CACHE
// ========================================
Future<void> _onClearCache(
_ClearCache event,
Emitter<CategoryLoaderState> emit,
) async {
log('🧹 Manually clearing category cache');
_categoryRepository.clearCache();
// Refresh current data after cache clear
add(const CategoryLoaderEvent.refresh());
} }
} }
File diff suppressed because it is too large Load Diff
@@ -2,7 +2,26 @@ part of 'category_loader_bloc.dart';
@freezed @freezed
class CategoryLoaderEvent with _$CategoryLoaderEvent { class CategoryLoaderEvent with _$CategoryLoaderEvent {
const factory CategoryLoaderEvent.get() = _Get; const factory CategoryLoaderEvent.getCategories({
const factory CategoryLoaderEvent.setCategoryId(String categoryId) = @Default(true) bool isActive,
_SetCategoryId; String? search,
@Default(false) bool forceRemote,
}) = _GetCategories;
const factory CategoryLoaderEvent.loadMore() = _LoadMore;
const factory CategoryLoaderEvent.refresh() = _Refresh;
const factory CategoryLoaderEvent.search({
required String query,
@Default(true) bool isActive,
}) = _Search;
const factory CategoryLoaderEvent.syncAll() = _SyncAll;
const factory CategoryLoaderEvent.getAllCategories() = _GetAllCategories;
const factory CategoryLoaderEvent.getDatabaseStats() = _GetDatabaseStats;
const factory CategoryLoaderEvent.clearCache() = _ClearCache;
} }
@@ -3,8 +3,29 @@ part of 'category_loader_bloc.dart';
@freezed @freezed
class CategoryLoaderState with _$CategoryLoaderState { class CategoryLoaderState with _$CategoryLoaderState {
const factory CategoryLoaderState.initial() = _Initial; const factory CategoryLoaderState.initial() = _Initial;
const factory CategoryLoaderState.loading() = _Loading; const factory CategoryLoaderState.loading() = _Loading;
const factory CategoryLoaderState.loaded(
List<CategoryModel> categories, String? categoryId) = _Loaded; const factory CategoryLoaderState.loaded({
required List<CategoryModel> categories,
required bool hasReachedMax,
required int currentPage,
required bool isLoadingMore,
required bool isActive,
String? searchQuery,
}) = _Loaded;
const factory CategoryLoaderState.error(String message) = _Error; const factory CategoryLoaderState.error(String message) = _Error;
// Sync-specific states
const factory CategoryLoaderState.syncing() = _Syncing;
const factory CategoryLoaderState.syncSuccess(String message) = _SyncSuccess;
const factory CategoryLoaderState.syncError(String message) = _SyncError;
// For dropdown/all categories
const factory CategoryLoaderState.allCategoriesLoaded(
List<CategoryModel> categories,
) = _AllCategoriesLoaded;
} }
@@ -1,7 +1,6 @@
import 'dart:developer'; import 'dart:developer';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:enaklo_pos/data/datasources/product_local_datasource.dart';
import 'package:enaklo_pos/data/models/response/delivery_response_model.dart'; import 'package:enaklo_pos/data/models/response/delivery_response_model.dart';
import 'package:enaklo_pos/data/models/response/discount_response_model.dart'; import 'package:enaklo_pos/data/models/response/discount_response_model.dart';
import 'package:enaklo_pos/presentation/table/models/draft_order_item.dart'; import 'package:enaklo_pos/presentation/table/models/draft_order_item.dart';
@@ -131,7 +130,7 @@ class CheckoutBloc extends Bloc<CheckoutEvent, CheckoutState> {
final serviceCharge = await settingsLocalDatasource.getServiceCharge(); final serviceCharge = await settingsLocalDatasource.getServiceCharge();
emit(_Loaded( emit(_Loaded(
[], event.items,
null, null,
0, 0,
0, 0,
@@ -310,9 +309,9 @@ class CheckoutBloc extends Bloc<CheckoutEvent, CheckoutState> {
DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now()), DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now()),
); );
log("draftOrder12: ${draftOrder.toMapForLocal()}"); log("draftOrder12: ${draftOrder.toMapForLocal()}");
final orderDraftId = // final orderDraftId =
await ProductLocalDatasource.instance.saveDraftOrder(draftOrder); // await ProductLocalDatasource.instance.saveDraftOrder(draftOrder);
emit(_SavedDraftOrder(orderDraftId)); emit(_SavedDraftOrder(0));
}); });
//load draft order //load draft order
@@ -18,7 +18,7 @@ final _privateConstructorUsedError = UnsupportedError(
mixin _$CheckoutEvent { mixin _$CheckoutEvent {
@optionalTypeArgs @optionalTypeArgs
TResult when<TResult extends Object?>({ TResult when<TResult extends Object?>({
required TResult Function() started, required TResult Function(List<ProductQuantity> items) started,
required TResult Function(Product product, ProductVariant? variant) addItem, required TResult Function(Product product, ProductVariant? variant) addItem,
required TResult Function(Product product, ProductVariant? variant) required TResult Function(Product product, ProductVariant? variant)
removeItem, removeItem,
@@ -41,7 +41,7 @@ mixin _$CheckoutEvent {
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
@optionalTypeArgs @optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({ TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? started, TResult? Function(List<ProductQuantity> items)? started,
TResult? Function(Product product, ProductVariant? variant)? addItem, TResult? Function(Product product, ProductVariant? variant)? addItem,
TResult? Function(Product product, ProductVariant? variant)? removeItem, TResult? Function(Product product, ProductVariant? variant)? removeItem,
TResult? Function(Product product, ProductVariant? variant)? deleteItem, TResult? Function(Product product, ProductVariant? variant)? deleteItem,
@@ -61,7 +61,7 @@ mixin _$CheckoutEvent {
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
@optionalTypeArgs @optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({ TResult maybeWhen<TResult extends Object?>({
TResult Function()? started, TResult Function(List<ProductQuantity> items)? started,
TResult Function(Product product, ProductVariant? variant)? addItem, TResult Function(Product product, ProductVariant? variant)? addItem,
TResult Function(Product product, ProductVariant? variant)? removeItem, TResult Function(Product product, ProductVariant? variant)? removeItem,
TResult Function(Product product, ProductVariant? variant)? deleteItem, TResult Function(Product product, ProductVariant? variant)? deleteItem,
@@ -166,6 +166,8 @@ abstract class _$$StartedImplCopyWith<$Res> {
factory _$$StartedImplCopyWith( factory _$$StartedImplCopyWith(
_$StartedImpl value, $Res Function(_$StartedImpl) then) = _$StartedImpl value, $Res Function(_$StartedImpl) then) =
__$$StartedImplCopyWithImpl<$Res>; __$$StartedImplCopyWithImpl<$Res>;
@useResult
$Res call({List<ProductQuantity> items});
} }
/// @nodoc /// @nodoc
@@ -178,31 +180,62 @@ class __$$StartedImplCopyWithImpl<$Res>
/// Create a copy of CheckoutEvent /// Create a copy of CheckoutEvent
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? items = null,
}) {
return _then(_$StartedImpl(
null == items
? _value._items
: items // ignore: cast_nullable_to_non_nullable
as List<ProductQuantity>,
));
}
} }
/// @nodoc /// @nodoc
class _$StartedImpl implements _Started { class _$StartedImpl implements _Started {
const _$StartedImpl(); const _$StartedImpl(final List<ProductQuantity> items) : _items = items;
final List<ProductQuantity> _items;
@override
List<ProductQuantity> get items {
if (_items is EqualUnmodifiableListView) return _items;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_items);
}
@override @override
String toString() { String toString() {
return 'CheckoutEvent.started()'; return 'CheckoutEvent.started(items: $items)';
} }
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || return identical(this, other) ||
(other.runtimeType == runtimeType && other is _$StartedImpl); (other.runtimeType == runtimeType &&
other is _$StartedImpl &&
const DeepCollectionEquality().equals(other._items, _items));
} }
@override @override
int get hashCode => runtimeType.hashCode; int get hashCode =>
Object.hash(runtimeType, const DeepCollectionEquality().hash(_items));
/// Create a copy of CheckoutEvent
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$StartedImplCopyWith<_$StartedImpl> get copyWith =>
__$$StartedImplCopyWithImpl<_$StartedImpl>(this, _$identity);
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult when<TResult extends Object?>({ TResult when<TResult extends Object?>({
required TResult Function() started, required TResult Function(List<ProductQuantity> items) started,
required TResult Function(Product product, ProductVariant? variant) addItem, required TResult Function(Product product, ProductVariant? variant) addItem,
required TResult Function(Product product, ProductVariant? variant) required TResult Function(Product product, ProductVariant? variant)
removeItem, removeItem,
@@ -222,13 +255,13 @@ class _$StartedImpl implements _Started {
required TResult Function(DraftOrderModel data) loadDraftOrder, required TResult Function(DraftOrderModel data) loadDraftOrder,
required TResult Function(DeliveryModel delivery) updateDeliveryType, required TResult Function(DeliveryModel delivery) updateDeliveryType,
}) { }) {
return started(); return started(items);
} }
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({ TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? started, TResult? Function(List<ProductQuantity> items)? started,
TResult? Function(Product product, ProductVariant? variant)? addItem, TResult? Function(Product product, ProductVariant? variant)? addItem,
TResult? Function(Product product, ProductVariant? variant)? removeItem, TResult? Function(Product product, ProductVariant? variant)? removeItem,
TResult? Function(Product product, ProductVariant? variant)? deleteItem, TResult? Function(Product product, ProductVariant? variant)? deleteItem,
@@ -245,13 +278,13 @@ class _$StartedImpl implements _Started {
TResult? Function(DraftOrderModel data)? loadDraftOrder, TResult? Function(DraftOrderModel data)? loadDraftOrder,
TResult? Function(DeliveryModel delivery)? updateDeliveryType, TResult? Function(DeliveryModel delivery)? updateDeliveryType,
}) { }) {
return started?.call(); return started?.call(items);
} }
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({ TResult maybeWhen<TResult extends Object?>({
TResult Function()? started, TResult Function(List<ProductQuantity> items)? started,
TResult Function(Product product, ProductVariant? variant)? addItem, TResult Function(Product product, ProductVariant? variant)? addItem,
TResult Function(Product product, ProductVariant? variant)? removeItem, TResult Function(Product product, ProductVariant? variant)? removeItem,
TResult Function(Product product, ProductVariant? variant)? deleteItem, TResult Function(Product product, ProductVariant? variant)? deleteItem,
@@ -270,7 +303,7 @@ class _$StartedImpl implements _Started {
required TResult orElse(), required TResult orElse(),
}) { }) {
if (started != null) { if (started != null) {
return started(); return started(items);
} }
return orElse(); return orElse();
} }
@@ -347,7 +380,15 @@ class _$StartedImpl implements _Started {
} }
abstract class _Started implements CheckoutEvent { abstract class _Started implements CheckoutEvent {
const factory _Started() = _$StartedImpl; const factory _Started(final List<ProductQuantity> items) = _$StartedImpl;
List<ProductQuantity> get items;
/// Create a copy of CheckoutEvent
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
_$$StartedImplCopyWith<_$StartedImpl> get copyWith =>
throw _privateConstructorUsedError;
} }
/// @nodoc /// @nodoc
@@ -426,7 +467,7 @@ class _$AddItemImpl implements _AddItem {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult when<TResult extends Object?>({ TResult when<TResult extends Object?>({
required TResult Function() started, required TResult Function(List<ProductQuantity> items) started,
required TResult Function(Product product, ProductVariant? variant) addItem, required TResult Function(Product product, ProductVariant? variant) addItem,
required TResult Function(Product product, ProductVariant? variant) required TResult Function(Product product, ProductVariant? variant)
removeItem, removeItem,
@@ -452,7 +493,7 @@ class _$AddItemImpl implements _AddItem {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({ TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? started, TResult? Function(List<ProductQuantity> items)? started,
TResult? Function(Product product, ProductVariant? variant)? addItem, TResult? Function(Product product, ProductVariant? variant)? addItem,
TResult? Function(Product product, ProductVariant? variant)? removeItem, TResult? Function(Product product, ProductVariant? variant)? removeItem,
TResult? Function(Product product, ProductVariant? variant)? deleteItem, TResult? Function(Product product, ProductVariant? variant)? deleteItem,
@@ -475,7 +516,7 @@ class _$AddItemImpl implements _AddItem {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({ TResult maybeWhen<TResult extends Object?>({
TResult Function()? started, TResult Function(List<ProductQuantity> items)? started,
TResult Function(Product product, ProductVariant? variant)? addItem, TResult Function(Product product, ProductVariant? variant)? addItem,
TResult Function(Product product, ProductVariant? variant)? removeItem, TResult Function(Product product, ProductVariant? variant)? removeItem,
TResult Function(Product product, ProductVariant? variant)? deleteItem, TResult Function(Product product, ProductVariant? variant)? deleteItem,
@@ -660,7 +701,7 @@ class _$RemoveItemImpl implements _RemoveItem {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult when<TResult extends Object?>({ TResult when<TResult extends Object?>({
required TResult Function() started, required TResult Function(List<ProductQuantity> items) started,
required TResult Function(Product product, ProductVariant? variant) addItem, required TResult Function(Product product, ProductVariant? variant) addItem,
required TResult Function(Product product, ProductVariant? variant) required TResult Function(Product product, ProductVariant? variant)
removeItem, removeItem,
@@ -686,7 +727,7 @@ class _$RemoveItemImpl implements _RemoveItem {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({ TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? started, TResult? Function(List<ProductQuantity> items)? started,
TResult? Function(Product product, ProductVariant? variant)? addItem, TResult? Function(Product product, ProductVariant? variant)? addItem,
TResult? Function(Product product, ProductVariant? variant)? removeItem, TResult? Function(Product product, ProductVariant? variant)? removeItem,
TResult? Function(Product product, ProductVariant? variant)? deleteItem, TResult? Function(Product product, ProductVariant? variant)? deleteItem,
@@ -709,7 +750,7 @@ class _$RemoveItemImpl implements _RemoveItem {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({ TResult maybeWhen<TResult extends Object?>({
TResult Function()? started, TResult Function(List<ProductQuantity> items)? started,
TResult Function(Product product, ProductVariant? variant)? addItem, TResult Function(Product product, ProductVariant? variant)? addItem,
TResult Function(Product product, ProductVariant? variant)? removeItem, TResult Function(Product product, ProductVariant? variant)? removeItem,
TResult Function(Product product, ProductVariant? variant)? deleteItem, TResult Function(Product product, ProductVariant? variant)? deleteItem,
@@ -894,7 +935,7 @@ class _$DeleteItemImpl implements _DeleteItem {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult when<TResult extends Object?>({ TResult when<TResult extends Object?>({
required TResult Function() started, required TResult Function(List<ProductQuantity> items) started,
required TResult Function(Product product, ProductVariant? variant) addItem, required TResult Function(Product product, ProductVariant? variant) addItem,
required TResult Function(Product product, ProductVariant? variant) required TResult Function(Product product, ProductVariant? variant)
removeItem, removeItem,
@@ -920,7 +961,7 @@ class _$DeleteItemImpl implements _DeleteItem {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({ TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? started, TResult? Function(List<ProductQuantity> items)? started,
TResult? Function(Product product, ProductVariant? variant)? addItem, TResult? Function(Product product, ProductVariant? variant)? addItem,
TResult? Function(Product product, ProductVariant? variant)? removeItem, TResult? Function(Product product, ProductVariant? variant)? removeItem,
TResult? Function(Product product, ProductVariant? variant)? deleteItem, TResult? Function(Product product, ProductVariant? variant)? deleteItem,
@@ -943,7 +984,7 @@ class _$DeleteItemImpl implements _DeleteItem {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({ TResult maybeWhen<TResult extends Object?>({
TResult Function()? started, TResult Function(List<ProductQuantity> items)? started,
TResult Function(Product product, ProductVariant? variant)? addItem, TResult Function(Product product, ProductVariant? variant)? addItem,
TResult Function(Product product, ProductVariant? variant)? removeItem, TResult Function(Product product, ProductVariant? variant)? removeItem,
TResult Function(Product product, ProductVariant? variant)? deleteItem, TResult Function(Product product, ProductVariant? variant)? deleteItem,
@@ -1121,7 +1162,7 @@ class _$AddDiscountImpl implements _AddDiscount {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult when<TResult extends Object?>({ TResult when<TResult extends Object?>({
required TResult Function() started, required TResult Function(List<ProductQuantity> items) started,
required TResult Function(Product product, ProductVariant? variant) addItem, required TResult Function(Product product, ProductVariant? variant) addItem,
required TResult Function(Product product, ProductVariant? variant) required TResult Function(Product product, ProductVariant? variant)
removeItem, removeItem,
@@ -1147,7 +1188,7 @@ class _$AddDiscountImpl implements _AddDiscount {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({ TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? started, TResult? Function(List<ProductQuantity> items)? started,
TResult? Function(Product product, ProductVariant? variant)? addItem, TResult? Function(Product product, ProductVariant? variant)? addItem,
TResult? Function(Product product, ProductVariant? variant)? removeItem, TResult? Function(Product product, ProductVariant? variant)? removeItem,
TResult? Function(Product product, ProductVariant? variant)? deleteItem, TResult? Function(Product product, ProductVariant? variant)? deleteItem,
@@ -1170,7 +1211,7 @@ class _$AddDiscountImpl implements _AddDiscount {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({ TResult maybeWhen<TResult extends Object?>({
TResult Function()? started, TResult Function(List<ProductQuantity> items)? started,
TResult Function(Product product, ProductVariant? variant)? addItem, TResult Function(Product product, ProductVariant? variant)? addItem,
TResult Function(Product product, ProductVariant? variant)? removeItem, TResult Function(Product product, ProductVariant? variant)? removeItem,
TResult Function(Product product, ProductVariant? variant)? deleteItem, TResult Function(Product product, ProductVariant? variant)? deleteItem,
@@ -1318,7 +1359,7 @@ class _$RemoveDiscountImpl implements _RemoveDiscount {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult when<TResult extends Object?>({ TResult when<TResult extends Object?>({
required TResult Function() started, required TResult Function(List<ProductQuantity> items) started,
required TResult Function(Product product, ProductVariant? variant) addItem, required TResult Function(Product product, ProductVariant? variant) addItem,
required TResult Function(Product product, ProductVariant? variant) required TResult Function(Product product, ProductVariant? variant)
removeItem, removeItem,
@@ -1344,7 +1385,7 @@ class _$RemoveDiscountImpl implements _RemoveDiscount {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({ TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? started, TResult? Function(List<ProductQuantity> items)? started,
TResult? Function(Product product, ProductVariant? variant)? addItem, TResult? Function(Product product, ProductVariant? variant)? addItem,
TResult? Function(Product product, ProductVariant? variant)? removeItem, TResult? Function(Product product, ProductVariant? variant)? removeItem,
TResult? Function(Product product, ProductVariant? variant)? deleteItem, TResult? Function(Product product, ProductVariant? variant)? deleteItem,
@@ -1367,7 +1408,7 @@ class _$RemoveDiscountImpl implements _RemoveDiscount {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({ TResult maybeWhen<TResult extends Object?>({
TResult Function()? started, TResult Function(List<ProductQuantity> items)? started,
TResult Function(Product product, ProductVariant? variant)? addItem, TResult Function(Product product, ProductVariant? variant)? addItem,
TResult Function(Product product, ProductVariant? variant)? removeItem, TResult Function(Product product, ProductVariant? variant)? removeItem,
TResult Function(Product product, ProductVariant? variant)? deleteItem, TResult Function(Product product, ProductVariant? variant)? deleteItem,
@@ -1534,7 +1575,7 @@ class _$AddTaxImpl implements _AddTax {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult when<TResult extends Object?>({ TResult when<TResult extends Object?>({
required TResult Function() started, required TResult Function(List<ProductQuantity> items) started,
required TResult Function(Product product, ProductVariant? variant) addItem, required TResult Function(Product product, ProductVariant? variant) addItem,
required TResult Function(Product product, ProductVariant? variant) required TResult Function(Product product, ProductVariant? variant)
removeItem, removeItem,
@@ -1560,7 +1601,7 @@ class _$AddTaxImpl implements _AddTax {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({ TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? started, TResult? Function(List<ProductQuantity> items)? started,
TResult? Function(Product product, ProductVariant? variant)? addItem, TResult? Function(Product product, ProductVariant? variant)? addItem,
TResult? Function(Product product, ProductVariant? variant)? removeItem, TResult? Function(Product product, ProductVariant? variant)? removeItem,
TResult? Function(Product product, ProductVariant? variant)? deleteItem, TResult? Function(Product product, ProductVariant? variant)? deleteItem,
@@ -1583,7 +1624,7 @@ class _$AddTaxImpl implements _AddTax {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({ TResult maybeWhen<TResult extends Object?>({
TResult Function()? started, TResult Function(List<ProductQuantity> items)? started,
TResult Function(Product product, ProductVariant? variant)? addItem, TResult Function(Product product, ProductVariant? variant)? addItem,
TResult Function(Product product, ProductVariant? variant)? removeItem, TResult Function(Product product, ProductVariant? variant)? removeItem,
TResult Function(Product product, ProductVariant? variant)? deleteItem, TResult Function(Product product, ProductVariant? variant)? deleteItem,
@@ -1760,7 +1801,7 @@ class _$AddServiceChargeImpl implements _AddServiceCharge {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult when<TResult extends Object?>({ TResult when<TResult extends Object?>({
required TResult Function() started, required TResult Function(List<ProductQuantity> items) started,
required TResult Function(Product product, ProductVariant? variant) addItem, required TResult Function(Product product, ProductVariant? variant) addItem,
required TResult Function(Product product, ProductVariant? variant) required TResult Function(Product product, ProductVariant? variant)
removeItem, removeItem,
@@ -1786,7 +1827,7 @@ class _$AddServiceChargeImpl implements _AddServiceCharge {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({ TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? started, TResult? Function(List<ProductQuantity> items)? started,
TResult? Function(Product product, ProductVariant? variant)? addItem, TResult? Function(Product product, ProductVariant? variant)? addItem,
TResult? Function(Product product, ProductVariant? variant)? removeItem, TResult? Function(Product product, ProductVariant? variant)? removeItem,
TResult? Function(Product product, ProductVariant? variant)? deleteItem, TResult? Function(Product product, ProductVariant? variant)? deleteItem,
@@ -1809,7 +1850,7 @@ class _$AddServiceChargeImpl implements _AddServiceCharge {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({ TResult maybeWhen<TResult extends Object?>({
TResult Function()? started, TResult Function(List<ProductQuantity> items)? started,
TResult Function(Product product, ProductVariant? variant)? addItem, TResult Function(Product product, ProductVariant? variant)? addItem,
TResult Function(Product product, ProductVariant? variant)? removeItem, TResult Function(Product product, ProductVariant? variant)? removeItem,
TResult Function(Product product, ProductVariant? variant)? deleteItem, TResult Function(Product product, ProductVariant? variant)? deleteItem,
@@ -1958,7 +1999,7 @@ class _$RemoveTaxImpl implements _RemoveTax {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult when<TResult extends Object?>({ TResult when<TResult extends Object?>({
required TResult Function() started, required TResult Function(List<ProductQuantity> items) started,
required TResult Function(Product product, ProductVariant? variant) addItem, required TResult Function(Product product, ProductVariant? variant) addItem,
required TResult Function(Product product, ProductVariant? variant) required TResult Function(Product product, ProductVariant? variant)
removeItem, removeItem,
@@ -1984,7 +2025,7 @@ class _$RemoveTaxImpl implements _RemoveTax {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({ TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? started, TResult? Function(List<ProductQuantity> items)? started,
TResult? Function(Product product, ProductVariant? variant)? addItem, TResult? Function(Product product, ProductVariant? variant)? addItem,
TResult? Function(Product product, ProductVariant? variant)? removeItem, TResult? Function(Product product, ProductVariant? variant)? removeItem,
TResult? Function(Product product, ProductVariant? variant)? deleteItem, TResult? Function(Product product, ProductVariant? variant)? deleteItem,
@@ -2007,7 +2048,7 @@ class _$RemoveTaxImpl implements _RemoveTax {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({ TResult maybeWhen<TResult extends Object?>({
TResult Function()? started, TResult Function(List<ProductQuantity> items)? started,
TResult Function(Product product, ProductVariant? variant)? addItem, TResult Function(Product product, ProductVariant? variant)? addItem,
TResult Function(Product product, ProductVariant? variant)? removeItem, TResult Function(Product product, ProductVariant? variant)? removeItem,
TResult Function(Product product, ProductVariant? variant)? deleteItem, TResult Function(Product product, ProductVariant? variant)? deleteItem,
@@ -2148,7 +2189,7 @@ class _$RemoveServiceChargeImpl implements _RemoveServiceCharge {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult when<TResult extends Object?>({ TResult when<TResult extends Object?>({
required TResult Function() started, required TResult Function(List<ProductQuantity> items) started,
required TResult Function(Product product, ProductVariant? variant) addItem, required TResult Function(Product product, ProductVariant? variant) addItem,
required TResult Function(Product product, ProductVariant? variant) required TResult Function(Product product, ProductVariant? variant)
removeItem, removeItem,
@@ -2174,7 +2215,7 @@ class _$RemoveServiceChargeImpl implements _RemoveServiceCharge {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({ TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? started, TResult? Function(List<ProductQuantity> items)? started,
TResult? Function(Product product, ProductVariant? variant)? addItem, TResult? Function(Product product, ProductVariant? variant)? addItem,
TResult? Function(Product product, ProductVariant? variant)? removeItem, TResult? Function(Product product, ProductVariant? variant)? removeItem,
TResult? Function(Product product, ProductVariant? variant)? deleteItem, TResult? Function(Product product, ProductVariant? variant)? deleteItem,
@@ -2197,7 +2238,7 @@ class _$RemoveServiceChargeImpl implements _RemoveServiceCharge {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({ TResult maybeWhen<TResult extends Object?>({
TResult Function()? started, TResult Function(List<ProductQuantity> items)? started,
TResult Function(Product product, ProductVariant? variant)? addItem, TResult Function(Product product, ProductVariant? variant)? addItem,
TResult Function(Product product, ProductVariant? variant)? removeItem, TResult Function(Product product, ProductVariant? variant)? removeItem,
TResult Function(Product product, ProductVariant? variant)? deleteItem, TResult Function(Product product, ProductVariant? variant)? deleteItem,
@@ -2366,7 +2407,7 @@ class _$UpdateOrderTypeImpl implements _UpdateOrderType {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult when<TResult extends Object?>({ TResult when<TResult extends Object?>({
required TResult Function() started, required TResult Function(List<ProductQuantity> items) started,
required TResult Function(Product product, ProductVariant? variant) addItem, required TResult Function(Product product, ProductVariant? variant) addItem,
required TResult Function(Product product, ProductVariant? variant) required TResult Function(Product product, ProductVariant? variant)
removeItem, removeItem,
@@ -2392,7 +2433,7 @@ class _$UpdateOrderTypeImpl implements _UpdateOrderType {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({ TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? started, TResult? Function(List<ProductQuantity> items)? started,
TResult? Function(Product product, ProductVariant? variant)? addItem, TResult? Function(Product product, ProductVariant? variant)? addItem,
TResult? Function(Product product, ProductVariant? variant)? removeItem, TResult? Function(Product product, ProductVariant? variant)? removeItem,
TResult? Function(Product product, ProductVariant? variant)? deleteItem, TResult? Function(Product product, ProductVariant? variant)? deleteItem,
@@ -2415,7 +2456,7 @@ class _$UpdateOrderTypeImpl implements _UpdateOrderType {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({ TResult maybeWhen<TResult extends Object?>({
TResult Function()? started, TResult Function(List<ProductQuantity> items)? started,
TResult Function(Product product, ProductVariant? variant)? addItem, TResult Function(Product product, ProductVariant? variant)? addItem,
TResult Function(Product product, ProductVariant? variant)? removeItem, TResult Function(Product product, ProductVariant? variant)? removeItem,
TResult Function(Product product, ProductVariant? variant)? deleteItem, TResult Function(Product product, ProductVariant? variant)? deleteItem,
@@ -2600,7 +2641,7 @@ class _$UpdateItemNotesImpl implements _UpdateItemNotes {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult when<TResult extends Object?>({ TResult when<TResult extends Object?>({
required TResult Function() started, required TResult Function(List<ProductQuantity> items) started,
required TResult Function(Product product, ProductVariant? variant) addItem, required TResult Function(Product product, ProductVariant? variant) addItem,
required TResult Function(Product product, ProductVariant? variant) required TResult Function(Product product, ProductVariant? variant)
removeItem, removeItem,
@@ -2626,7 +2667,7 @@ class _$UpdateItemNotesImpl implements _UpdateItemNotes {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({ TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? started, TResult? Function(List<ProductQuantity> items)? started,
TResult? Function(Product product, ProductVariant? variant)? addItem, TResult? Function(Product product, ProductVariant? variant)? addItem,
TResult? Function(Product product, ProductVariant? variant)? removeItem, TResult? Function(Product product, ProductVariant? variant)? removeItem,
TResult? Function(Product product, ProductVariant? variant)? deleteItem, TResult? Function(Product product, ProductVariant? variant)? deleteItem,
@@ -2649,7 +2690,7 @@ class _$UpdateItemNotesImpl implements _UpdateItemNotes {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({ TResult maybeWhen<TResult extends Object?>({
TResult Function()? started, TResult Function(List<ProductQuantity> items)? started,
TResult Function(Product product, ProductVariant? variant)? addItem, TResult Function(Product product, ProductVariant? variant)? addItem,
TResult Function(Product product, ProductVariant? variant)? removeItem, TResult Function(Product product, ProductVariant? variant)? removeItem,
TResult Function(Product product, ProductVariant? variant)? deleteItem, TResult Function(Product product, ProductVariant? variant)? deleteItem,
@@ -2848,7 +2889,7 @@ class _$SaveDraftOrderImpl implements _SaveDraftOrder {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult when<TResult extends Object?>({ TResult when<TResult extends Object?>({
required TResult Function() started, required TResult Function(List<ProductQuantity> items) started,
required TResult Function(Product product, ProductVariant? variant) addItem, required TResult Function(Product product, ProductVariant? variant) addItem,
required TResult Function(Product product, ProductVariant? variant) required TResult Function(Product product, ProductVariant? variant)
removeItem, removeItem,
@@ -2874,7 +2915,7 @@ class _$SaveDraftOrderImpl implements _SaveDraftOrder {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({ TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? started, TResult? Function(List<ProductQuantity> items)? started,
TResult? Function(Product product, ProductVariant? variant)? addItem, TResult? Function(Product product, ProductVariant? variant)? addItem,
TResult? Function(Product product, ProductVariant? variant)? removeItem, TResult? Function(Product product, ProductVariant? variant)? removeItem,
TResult? Function(Product product, ProductVariant? variant)? deleteItem, TResult? Function(Product product, ProductVariant? variant)? deleteItem,
@@ -2897,7 +2938,7 @@ class _$SaveDraftOrderImpl implements _SaveDraftOrder {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({ TResult maybeWhen<TResult extends Object?>({
TResult Function()? started, TResult Function(List<ProductQuantity> items)? started,
TResult Function(Product product, ProductVariant? variant)? addItem, TResult Function(Product product, ProductVariant? variant)? addItem,
TResult Function(Product product, ProductVariant? variant)? removeItem, TResult Function(Product product, ProductVariant? variant)? removeItem,
TResult Function(Product product, ProductVariant? variant)? deleteItem, TResult Function(Product product, ProductVariant? variant)? deleteItem,
@@ -3076,7 +3117,7 @@ class _$LoadDraftOrderImpl implements _LoadDraftOrder {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult when<TResult extends Object?>({ TResult when<TResult extends Object?>({
required TResult Function() started, required TResult Function(List<ProductQuantity> items) started,
required TResult Function(Product product, ProductVariant? variant) addItem, required TResult Function(Product product, ProductVariant? variant) addItem,
required TResult Function(Product product, ProductVariant? variant) required TResult Function(Product product, ProductVariant? variant)
removeItem, removeItem,
@@ -3102,7 +3143,7 @@ class _$LoadDraftOrderImpl implements _LoadDraftOrder {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({ TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? started, TResult? Function(List<ProductQuantity> items)? started,
TResult? Function(Product product, ProductVariant? variant)? addItem, TResult? Function(Product product, ProductVariant? variant)? addItem,
TResult? Function(Product product, ProductVariant? variant)? removeItem, TResult? Function(Product product, ProductVariant? variant)? removeItem,
TResult? Function(Product product, ProductVariant? variant)? deleteItem, TResult? Function(Product product, ProductVariant? variant)? deleteItem,
@@ -3125,7 +3166,7 @@ class _$LoadDraftOrderImpl implements _LoadDraftOrder {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({ TResult maybeWhen<TResult extends Object?>({
TResult Function()? started, TResult Function(List<ProductQuantity> items)? started,
TResult Function(Product product, ProductVariant? variant)? addItem, TResult Function(Product product, ProductVariant? variant)? addItem,
TResult Function(Product product, ProductVariant? variant)? removeItem, TResult Function(Product product, ProductVariant? variant)? removeItem,
TResult Function(Product product, ProductVariant? variant)? deleteItem, TResult Function(Product product, ProductVariant? variant)? deleteItem,
@@ -3303,7 +3344,7 @@ class _$UpdateDeliveryTypeImpl implements _UpdateDeliveryType {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult when<TResult extends Object?>({ TResult when<TResult extends Object?>({
required TResult Function() started, required TResult Function(List<ProductQuantity> items) started,
required TResult Function(Product product, ProductVariant? variant) addItem, required TResult Function(Product product, ProductVariant? variant) addItem,
required TResult Function(Product product, ProductVariant? variant) required TResult Function(Product product, ProductVariant? variant)
removeItem, removeItem,
@@ -3329,7 +3370,7 @@ class _$UpdateDeliveryTypeImpl implements _UpdateDeliveryType {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({ TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? started, TResult? Function(List<ProductQuantity> items)? started,
TResult? Function(Product product, ProductVariant? variant)? addItem, TResult? Function(Product product, ProductVariant? variant)? addItem,
TResult? Function(Product product, ProductVariant? variant)? removeItem, TResult? Function(Product product, ProductVariant? variant)? removeItem,
TResult? Function(Product product, ProductVariant? variant)? deleteItem, TResult? Function(Product product, ProductVariant? variant)? deleteItem,
@@ -3352,7 +3393,7 @@ class _$UpdateDeliveryTypeImpl implements _UpdateDeliveryType {
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({ TResult maybeWhen<TResult extends Object?>({
TResult Function()? started, TResult Function(List<ProductQuantity> items)? started,
TResult Function(Product product, ProductVariant? variant)? addItem, TResult Function(Product product, ProductVariant? variant)? addItem,
TResult Function(Product product, ProductVariant? variant)? removeItem, TResult Function(Product product, ProductVariant? variant)? removeItem,
TResult Function(Product product, ProductVariant? variant)? deleteItem, TResult Function(Product product, ProductVariant? variant)? deleteItem,
@@ -2,7 +2,7 @@ part of 'checkout_bloc.dart';
@freezed @freezed
class CheckoutEvent with _$CheckoutEvent { class CheckoutEvent with _$CheckoutEvent {
const factory CheckoutEvent.started() = _Started; const factory CheckoutEvent.started(List<ProductQuantity> items) = _Started;
//add item //add item
const factory CheckoutEvent.addItem( const factory CheckoutEvent.addItem(
Product product, ProductVariant? variant) = _AddItem; Product product, ProductVariant? variant) = _AddItem;
@@ -1,5 +1,5 @@
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:enaklo_pos/data/datasources/outlet_remote_data_source.dart'; import 'package:enaklo_pos/data/datasources/outlet/outlet_remote_data_source.dart';
import 'package:enaklo_pos/presentation/home/models/outlet_model.dart'; import 'package:enaklo_pos/presentation/home/models/outlet_model.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
@@ -1,26 +0,0 @@
import 'dart:developer';
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:enaklo_pos/data/datasources/product_local_datasource.dart';
import '../../../../data/models/response/product_response_model.dart';
part 'local_product_bloc.freezed.dart';
part 'local_product_event.dart';
part 'local_product_state.dart';
class LocalProductBloc extends Bloc<LocalProductEvent, LocalProductState> {
final ProductLocalDatasource productLocalDatasource;
LocalProductBloc(
this.productLocalDatasource,
) : super(const _Initial()) {
on<_GetLocalProduct>((event, emit) async {
emit(const _Loading());
final result = await productLocalDatasource.getProducts();
log("Result: ${result.length}");
emit(_Loaded(result));
});
}
}
@@ -1,7 +0,0 @@
part of 'local_product_bloc.dart';
@freezed
class LocalProductEvent with _$LocalProductEvent {
const factory LocalProductEvent.started() = _Started;
const factory LocalProductEvent.getLocalProduct() = _GetLocalProduct;
}
@@ -1,9 +0,0 @@
part of 'local_product_bloc.dart';
@freezed
class LocalProductState with _$LocalProductState {
const factory LocalProductState.initial() = _Initial;
const factory LocalProductState.loading() = _Loading;
const factory LocalProductState.loaded(List<Product> products) = _Loaded;
const factory LocalProductState.error(String message) = _Error;
}
@@ -5,7 +5,6 @@ import 'package:freezed_annotation/freezed_annotation.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/datasources/order_remote_datasource.dart'; import 'package:enaklo_pos/data/datasources/order_remote_datasource.dart';
import 'package:enaklo_pos/data/datasources/product_local_datasource.dart';
import '../../models/order_model.dart'; import '../../models/order_model.dart';
import '../../models/product_quantity.dart'; import '../../models/product_quantity.dart';
@@ -78,13 +77,13 @@ class OrderBloc extends Bloc<OrderEvent, OrderState> {
value = false; value = false;
} }
int id = 0; int id = 0;
if (value) { // if (value) {
id = await ProductLocalDatasource.instance // id = await ProductLocalDatasource.instance
.saveOrder(dataInput.copyWith(isSync: 1)); // .saveOrder(dataInput.copyWith(isSync: 1));
} else { // } else {
id = await ProductLocalDatasource.instance // id = await ProductLocalDatasource.instance
.saveOrder(dataInput.copyWith(isSync: 1)); // .saveOrder(dataInput.copyWith(isSync: 1));
} // }
emit(_Loaded( emit(_Loaded(
dataInput, dataInput,
@@ -77,8 +77,8 @@ class OrderFormBloc extends Bloc<OrderFormEvent, OrderFormState> {
customerName: event.customerName, customerName: event.customerName,
notes: '', notes: '',
orderType: event.orderType.name, orderType: event.orderType.name,
tableId: event.table.id, tableId: event.table?.id ?? "",
tableNumber: event.table.tableName, tableNumber: event.table?.tableName ?? "",
outletId: userData.user?.outletId, outletId: userData.user?.outletId,
customerId: event.customer?.id ?? '', customerId: event.customer?.id ?? '',
orderItems: event.items orderItems: event.items
@@ -131,11 +131,11 @@ class OrderFormBloc extends Bloc<OrderFormEvent, OrderFormState> {
result.fold( result.fold(
(error) => emit(_Error(error)), (error) => emit(_Error(error)),
(success) => emit(_Success(success.data!)), (success) => emit(_SuccessMsg()),
); );
} catch (e) { } catch (e) {
log("Error in AddOrderItemsBloc: $e"); log("Error in AddOrderItemsBloc: $e");
emit(_Error("Failed to add order items: $e")); emit(_Error("Ada kesalahan. Coba lagi nanti"));
} }
}, },
); );
@@ -27,7 +27,7 @@ mixin _$OrderFormEvent {
String customerName, String customerName,
Customer? customer, Customer? customer,
OrderType orderType, OrderType orderType,
TableModel table, TableModel? table,
PaymentMethod paymentMethod) PaymentMethod paymentMethod)
createWithPayment, createWithPayment,
required TResult Function(List<ProductQuantity> items, String orderId) required TResult Function(List<ProductQuantity> items, String orderId)
@@ -53,7 +53,7 @@ mixin _$OrderFormEvent {
String customerName, String customerName,
Customer? customer, Customer? customer,
OrderType orderType, OrderType orderType,
TableModel table, TableModel? table,
PaymentMethod paymentMethod)? PaymentMethod paymentMethod)?
createWithPayment, createWithPayment,
TResult? Function(List<ProductQuantity> items, String orderId)? addToOrder, TResult? Function(List<ProductQuantity> items, String orderId)? addToOrder,
@@ -76,7 +76,7 @@ mixin _$OrderFormEvent {
String customerName, String customerName,
Customer? customer, Customer? customer,
OrderType orderType, OrderType orderType,
TableModel table, TableModel? table,
PaymentMethod paymentMethod)? PaymentMethod paymentMethod)?
createWithPayment, createWithPayment,
TResult Function(List<ProductQuantity> items, String orderId)? addToOrder, TResult Function(List<ProductQuantity> items, String orderId)? addToOrder,
@@ -226,7 +226,7 @@ class _$StartedImpl implements _Started {
String customerName, String customerName,
Customer? customer, Customer? customer,
OrderType orderType, OrderType orderType,
TableModel table, TableModel? table,
PaymentMethod paymentMethod) PaymentMethod paymentMethod)
createWithPayment, createWithPayment,
required TResult Function(List<ProductQuantity> items, String orderId) required TResult Function(List<ProductQuantity> items, String orderId)
@@ -255,7 +255,7 @@ class _$StartedImpl implements _Started {
String customerName, String customerName,
Customer? customer, Customer? customer,
OrderType orderType, OrderType orderType,
TableModel table, TableModel? table,
PaymentMethod paymentMethod)? PaymentMethod paymentMethod)?
createWithPayment, createWithPayment,
TResult? Function(List<ProductQuantity> items, String orderId)? addToOrder, TResult? Function(List<ProductQuantity> items, String orderId)? addToOrder,
@@ -281,7 +281,7 @@ class _$StartedImpl implements _Started {
String customerName, String customerName,
Customer? customer, Customer? customer,
OrderType orderType, OrderType orderType,
TableModel table, TableModel? table,
PaymentMethod paymentMethod)? PaymentMethod paymentMethod)?
createWithPayment, createWithPayment,
TResult Function(List<ProductQuantity> items, String orderId)? addToOrder, TResult Function(List<ProductQuantity> items, String orderId)? addToOrder,
@@ -496,7 +496,7 @@ class _$CreateImpl implements _Create {
String customerName, String customerName,
Customer? customer, Customer? customer,
OrderType orderType, OrderType orderType,
TableModel table, TableModel? table,
PaymentMethod paymentMethod) PaymentMethod paymentMethod)
createWithPayment, createWithPayment,
required TResult Function(List<ProductQuantity> items, String orderId) required TResult Function(List<ProductQuantity> items, String orderId)
@@ -525,7 +525,7 @@ class _$CreateImpl implements _Create {
String customerName, String customerName,
Customer? customer, Customer? customer,
OrderType orderType, OrderType orderType,
TableModel table, TableModel? table,
PaymentMethod paymentMethod)? PaymentMethod paymentMethod)?
createWithPayment, createWithPayment,
TResult? Function(List<ProductQuantity> items, String orderId)? addToOrder, TResult? Function(List<ProductQuantity> items, String orderId)? addToOrder,
@@ -551,7 +551,7 @@ class _$CreateImpl implements _Create {
String customerName, String customerName,
Customer? customer, Customer? customer,
OrderType orderType, OrderType orderType,
TableModel table, TableModel? table,
PaymentMethod paymentMethod)? PaymentMethod paymentMethod)?
createWithPayment, createWithPayment,
TResult Function(List<ProductQuantity> items, String orderId)? addToOrder, TResult Function(List<ProductQuantity> items, String orderId)? addToOrder,
@@ -652,7 +652,7 @@ abstract class _$$CreateWithPaymentMethodImplCopyWith<$Res> {
String customerName, String customerName,
Customer? customer, Customer? customer,
OrderType orderType, OrderType orderType,
TableModel table, TableModel? table,
PaymentMethod paymentMethod}); PaymentMethod paymentMethod});
} }
@@ -674,7 +674,7 @@ class __$$CreateWithPaymentMethodImplCopyWithImpl<$Res>
Object? customerName = null, Object? customerName = null,
Object? customer = freezed, Object? customer = freezed,
Object? orderType = null, Object? orderType = null,
Object? table = null, Object? table = freezed,
Object? paymentMethod = null, Object? paymentMethod = null,
}) { }) {
return _then(_$CreateWithPaymentMethodImpl( return _then(_$CreateWithPaymentMethodImpl(
@@ -694,10 +694,10 @@ class __$$CreateWithPaymentMethodImplCopyWithImpl<$Res>
? _value.orderType ? _value.orderType
: orderType // ignore: cast_nullable_to_non_nullable : orderType // ignore: cast_nullable_to_non_nullable
as OrderType, as OrderType,
table: null == table table: freezed == table
? _value.table ? _value.table
: table // ignore: cast_nullable_to_non_nullable : table // ignore: cast_nullable_to_non_nullable
as TableModel, as TableModel?,
paymentMethod: null == paymentMethod paymentMethod: null == paymentMethod
? _value.paymentMethod ? _value.paymentMethod
: paymentMethod // ignore: cast_nullable_to_non_nullable : paymentMethod // ignore: cast_nullable_to_non_nullable
@@ -733,7 +733,7 @@ class _$CreateWithPaymentMethodImpl implements _CreateWithPaymentMethod {
@override @override
final OrderType orderType; final OrderType orderType;
@override @override
final TableModel table; final TableModel? table;
@override @override
final PaymentMethod paymentMethod; final PaymentMethod paymentMethod;
@@ -790,7 +790,7 @@ class _$CreateWithPaymentMethodImpl implements _CreateWithPaymentMethod {
String customerName, String customerName,
Customer? customer, Customer? customer,
OrderType orderType, OrderType orderType,
TableModel table, TableModel? table,
PaymentMethod paymentMethod) PaymentMethod paymentMethod)
createWithPayment, createWithPayment,
required TResult Function(List<ProductQuantity> items, String orderId) required TResult Function(List<ProductQuantity> items, String orderId)
@@ -820,7 +820,7 @@ class _$CreateWithPaymentMethodImpl implements _CreateWithPaymentMethod {
String customerName, String customerName,
Customer? customer, Customer? customer,
OrderType orderType, OrderType orderType,
TableModel table, TableModel? table,
PaymentMethod paymentMethod)? PaymentMethod paymentMethod)?
createWithPayment, createWithPayment,
TResult? Function(List<ProductQuantity> items, String orderId)? addToOrder, TResult? Function(List<ProductQuantity> items, String orderId)? addToOrder,
@@ -847,7 +847,7 @@ class _$CreateWithPaymentMethodImpl implements _CreateWithPaymentMethod {
String customerName, String customerName,
Customer? customer, Customer? customer,
OrderType orderType, OrderType orderType,
TableModel table, TableModel? table,
PaymentMethod paymentMethod)? PaymentMethod paymentMethod)?
createWithPayment, createWithPayment,
TResult Function(List<ProductQuantity> items, String orderId)? addToOrder, TResult Function(List<ProductQuantity> items, String orderId)? addToOrder,
@@ -922,7 +922,7 @@ abstract class _CreateWithPaymentMethod implements OrderFormEvent {
required final String customerName, required final String customerName,
required final Customer? customer, required final Customer? customer,
required final OrderType orderType, required final OrderType orderType,
required final TableModel table, required final TableModel? table,
required final PaymentMethod paymentMethod}) = required final PaymentMethod paymentMethod}) =
_$CreateWithPaymentMethodImpl; _$CreateWithPaymentMethodImpl;
@@ -930,7 +930,7 @@ abstract class _CreateWithPaymentMethod implements OrderFormEvent {
String get customerName; String get customerName;
Customer? get customer; Customer? get customer;
OrderType get orderType; OrderType get orderType;
TableModel get table; TableModel? get table;
PaymentMethod get paymentMethod; PaymentMethod get paymentMethod;
/// Create a copy of OrderFormEvent /// Create a copy of OrderFormEvent
@@ -1034,7 +1034,7 @@ class _$AddToOrderImpl implements _AddToOrder {
String customerName, String customerName,
Customer? customer, Customer? customer,
OrderType orderType, OrderType orderType,
TableModel table, TableModel? table,
PaymentMethod paymentMethod) PaymentMethod paymentMethod)
createWithPayment, createWithPayment,
required TResult Function(List<ProductQuantity> items, String orderId) required TResult Function(List<ProductQuantity> items, String orderId)
@@ -1063,7 +1063,7 @@ class _$AddToOrderImpl implements _AddToOrder {
String customerName, String customerName,
Customer? customer, Customer? customer,
OrderType orderType, OrderType orderType,
TableModel table, TableModel? table,
PaymentMethod paymentMethod)? PaymentMethod paymentMethod)?
createWithPayment, createWithPayment,
TResult? Function(List<ProductQuantity> items, String orderId)? addToOrder, TResult? Function(List<ProductQuantity> items, String orderId)? addToOrder,
@@ -1089,7 +1089,7 @@ class _$AddToOrderImpl implements _AddToOrder {
String customerName, String customerName,
Customer? customer, Customer? customer,
OrderType orderType, OrderType orderType,
TableModel table, TableModel? table,
PaymentMethod paymentMethod)? PaymentMethod paymentMethod)?
createWithPayment, createWithPayment,
TResult Function(List<ProductQuantity> items, String orderId)? addToOrder, TResult Function(List<ProductQuantity> items, String orderId)? addToOrder,
@@ -1276,7 +1276,7 @@ class _$RefundImpl implements _Refund {
String customerName, String customerName,
Customer? customer, Customer? customer,
OrderType orderType, OrderType orderType,
TableModel table, TableModel? table,
PaymentMethod paymentMethod) PaymentMethod paymentMethod)
createWithPayment, createWithPayment,
required TResult Function(List<ProductQuantity> items, String orderId) required TResult Function(List<ProductQuantity> items, String orderId)
@@ -1305,7 +1305,7 @@ class _$RefundImpl implements _Refund {
String customerName, String customerName,
Customer? customer, Customer? customer,
OrderType orderType, OrderType orderType,
TableModel table, TableModel? table,
PaymentMethod paymentMethod)? PaymentMethod paymentMethod)?
createWithPayment, createWithPayment,
TResult? Function(List<ProductQuantity> items, String orderId)? addToOrder, TResult? Function(List<ProductQuantity> items, String orderId)? addToOrder,
@@ -1331,7 +1331,7 @@ class _$RefundImpl implements _Refund {
String customerName, String customerName,
Customer? customer, Customer? customer,
OrderType orderType, OrderType orderType,
TableModel table, TableModel? table,
PaymentMethod paymentMethod)? PaymentMethod paymentMethod)?
createWithPayment, createWithPayment,
TResult Function(List<ProductQuantity> items, String orderId)? addToOrder, TResult Function(List<ProductQuantity> items, String orderId)? addToOrder,
@@ -1520,7 +1520,7 @@ class _$VoidOrderImpl implements _VoidOrder {
String customerName, String customerName,
Customer? customer, Customer? customer,
OrderType orderType, OrderType orderType,
TableModel table, TableModel? table,
PaymentMethod paymentMethod) PaymentMethod paymentMethod)
createWithPayment, createWithPayment,
required TResult Function(List<ProductQuantity> items, String orderId) required TResult Function(List<ProductQuantity> items, String orderId)
@@ -1549,7 +1549,7 @@ class _$VoidOrderImpl implements _VoidOrder {
String customerName, String customerName,
Customer? customer, Customer? customer,
OrderType orderType, OrderType orderType,
TableModel table, TableModel? table,
PaymentMethod paymentMethod)? PaymentMethod paymentMethod)?
createWithPayment, createWithPayment,
TResult? Function(List<ProductQuantity> items, String orderId)? addToOrder, TResult? Function(List<ProductQuantity> items, String orderId)? addToOrder,
@@ -1575,7 +1575,7 @@ class _$VoidOrderImpl implements _VoidOrder {
String customerName, String customerName,
Customer? customer, Customer? customer,
OrderType orderType, OrderType orderType,
TableModel table, TableModel? table,
PaymentMethod paymentMethod)? PaymentMethod paymentMethod)?
createWithPayment, createWithPayment,
TResult Function(List<ProductQuantity> items, String orderId)? addToOrder, TResult Function(List<ProductQuantity> items, String orderId)? addToOrder,
@@ -1737,7 +1737,7 @@ class _$ToggleItemImpl implements _ToggleItem {
String customerName, String customerName,
Customer? customer, Customer? customer,
OrderType orderType, OrderType orderType,
TableModel table, TableModel? table,
PaymentMethod paymentMethod) PaymentMethod paymentMethod)
createWithPayment, createWithPayment,
required TResult Function(List<ProductQuantity> items, String orderId) required TResult Function(List<ProductQuantity> items, String orderId)
@@ -1766,7 +1766,7 @@ class _$ToggleItemImpl implements _ToggleItem {
String customerName, String customerName,
Customer? customer, Customer? customer,
OrderType orderType, OrderType orderType,
TableModel table, TableModel? table,
PaymentMethod paymentMethod)? PaymentMethod paymentMethod)?
createWithPayment, createWithPayment,
TResult? Function(List<ProductQuantity> items, String orderId)? addToOrder, TResult? Function(List<ProductQuantity> items, String orderId)? addToOrder,
@@ -1792,7 +1792,7 @@ class _$ToggleItemImpl implements _ToggleItem {
String customerName, String customerName,
Customer? customer, Customer? customer,
OrderType orderType, OrderType orderType,
TableModel table, TableModel? table,
PaymentMethod paymentMethod)? PaymentMethod paymentMethod)?
createWithPayment, createWithPayment,
TResult Function(List<ProductQuantity> items, String orderId)? addToOrder, TResult Function(List<ProductQuantity> items, String orderId)? addToOrder,
@@ -1951,7 +1951,7 @@ class _$ToggleSelectAllImpl implements _ToggleSelectAll {
String customerName, String customerName,
Customer? customer, Customer? customer,
OrderType orderType, OrderType orderType,
TableModel table, TableModel? table,
PaymentMethod paymentMethod) PaymentMethod paymentMethod)
createWithPayment, createWithPayment,
required TResult Function(List<ProductQuantity> items, String orderId) required TResult Function(List<ProductQuantity> items, String orderId)
@@ -1980,7 +1980,7 @@ class _$ToggleSelectAllImpl implements _ToggleSelectAll {
String customerName, String customerName,
Customer? customer, Customer? customer,
OrderType orderType, OrderType orderType,
TableModel table, TableModel? table,
PaymentMethod paymentMethod)? PaymentMethod paymentMethod)?
createWithPayment, createWithPayment,
TResult? Function(List<ProductQuantity> items, String orderId)? addToOrder, TResult? Function(List<ProductQuantity> items, String orderId)? addToOrder,
@@ -2006,7 +2006,7 @@ class _$ToggleSelectAllImpl implements _ToggleSelectAll {
String customerName, String customerName,
Customer? customer, Customer? customer,
OrderType orderType, OrderType orderType,
TableModel table, TableModel? table,
PaymentMethod paymentMethod)? PaymentMethod paymentMethod)?
createWithPayment, createWithPayment,
TResult Function(List<ProductQuantity> items, String orderId)? addToOrder, TResult Function(List<ProductQuantity> items, String orderId)? addToOrder,
@@ -15,7 +15,7 @@ class OrderFormEvent with _$OrderFormEvent {
required String customerName, required String customerName,
required Customer? customer, required Customer? customer,
required OrderType orderType, required OrderType orderType,
required TableModel table, required TableModel? table,
required PaymentMethod paymentMethod, required PaymentMethod paymentMethod,
}) = _CreateWithPaymentMethod; }) = _CreateWithPaymentMethod;
const factory OrderFormEvent.addToOrder({ const factory OrderFormEvent.addToOrder({

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