From 45b348684ec0a8caea32062afa44b755c09c6566 Mon Sep 17 00:00:00 2001 From: efrilm Date: Sat, 2 Aug 2025 10:50:48 +0700 Subject: [PATCH] feat: sales page --- lib/core/components/dashed_divider.dart | 41 +++ .../datasources/product_local_datasource.dart | 66 +++-- .../home/pages/confirm_payment_page.dart | 41 ++- .../home/widgets/home_right_title.dart | 3 +- .../sales/blocs/day_sales/day_sales_bloc.dart | 8 +- .../day_sales/day_sales_bloc.freezed.dart | 181 ++++++++++++ .../blocs/day_sales/day_sales_event.dart | 4 + .../sales/dialog/filter_dialog.dart | 123 ++++++++ lib/presentation/sales/pages/sales_page.dart | 274 +++++++++++------- .../sales/pages/sales_page.dart.backup | 127 ++++++++ .../sales/widgets/sales_card.dart | 113 ++++++++ .../sales/widgets/sales_detail.dart | 76 +++++ .../sales/widgets/sales_list_order.dart | 88 ++++++ .../widgets/sales_order_information.dart | 76 +++++ .../sales/widgets/sales_payment.dart | 99 +++++++ .../sales/widgets/sales_right_title.dart | 43 +++ .../sales/widgets/sales_title.dart | 146 ++++++++++ 17 files changed, 1371 insertions(+), 138 deletions(-) create mode 100644 lib/core/components/dashed_divider.dart create mode 100644 lib/presentation/sales/dialog/filter_dialog.dart create mode 100644 lib/presentation/sales/pages/sales_page.dart.backup create mode 100644 lib/presentation/sales/widgets/sales_card.dart create mode 100644 lib/presentation/sales/widgets/sales_detail.dart create mode 100644 lib/presentation/sales/widgets/sales_list_order.dart create mode 100644 lib/presentation/sales/widgets/sales_order_information.dart create mode 100644 lib/presentation/sales/widgets/sales_payment.dart create mode 100644 lib/presentation/sales/widgets/sales_right_title.dart create mode 100644 lib/presentation/sales/widgets/sales_title.dart diff --git a/lib/core/components/dashed_divider.dart b/lib/core/components/dashed_divider.dart new file mode 100644 index 0000000..5cb3fad --- /dev/null +++ b/lib/core/components/dashed_divider.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; + +class DashedDivider extends StatelessWidget { + final double height; + final double dashWidth; + final double dashSpacing; + final Color color; + + const DashedDivider({ + super.key, + this.height = 1, + this.dashWidth = 5, + this.dashSpacing = 3, + this.color = Colors.grey, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: height, + child: LayoutBuilder( + builder: (context, constraints) { + final boxWidth = constraints.constrainWidth(); + final dashCount = (boxWidth / (dashWidth + dashSpacing)).floor(); + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: List.generate(dashCount, (_) { + return SizedBox( + width: dashWidth, + height: height, + child: DecoratedBox( + decoration: BoxDecoration(color: color), + ), + ); + }), + ); + }, + ), + ); + } +} diff --git a/lib/data/datasources/product_local_datasource.dart b/lib/data/datasources/product_local_datasource.dart index 711d574..1ac0e68 100644 --- a/lib/data/datasources/product_local_datasource.dart +++ b/lib/data/datasources/product_local_datasource.dart @@ -7,7 +7,6 @@ import 'package:enaklo_pos/data/models/response/table_model.dart'; import 'package:enaklo_pos/presentation/home/models/order_model.dart'; import 'package:enaklo_pos/presentation/table/models/draft_order_item.dart'; import 'package:enaklo_pos/presentation/table/models/draft_order_model.dart'; -import 'package:intl/intl.dart'; import 'package:sqflite/sqflite.dart'; import '../../presentation/home/models/product_quantity.dart'; @@ -145,7 +144,7 @@ class ProductLocalDatasource { Future _initDB(String filePath) async { final dbPath = await getDatabasesPath(); final path = dbPath + filePath; - + // Force delete existing database to ensure new schema try { final dbExists = await databaseExists(path); @@ -156,20 +155,21 @@ class ProductLocalDatasource { } catch (e) { log("Error deleting database: $e"); } - + return await openDatabase( - path, - version: 2, + path, + version: 2, onCreate: _createDb, onUpgrade: _onUpgrade, ); } - + Future _onUpgrade(Database db, int oldVersion, int newVersion) async { if (oldVersion < 2) { // Add order_type column to orders table if it doesn't exist try { - await db.execute('ALTER TABLE $tableOrder ADD COLUMN order_type TEXT DEFAULT "DINE IN"'); + await db.execute( + 'ALTER TABLE $tableOrder ADD COLUMN order_type TEXT DEFAULT "DINE IN"'); log("Added order_type column to orders table"); } catch (e) { log("order_type column might already exist: $e"); @@ -186,11 +186,11 @@ class ProductLocalDatasource { //save order Future saveOrder(OrderModel order) async { final db = await instance.database; - + // Since we're forcing database recreation, order_type column should exist final orderMap = order.toMap(includeOrderType: true); log("Final orderMap for insertion: $orderMap"); - + int id = await db.insert(tableOrder, orderMap, conflictAlgorithm: ConflictAlgorithm.replace); @@ -238,6 +238,31 @@ class ProductLocalDatasource { }); } + Future> getAllOrderByRange( + DateTime start, DateTime end) async { + final db = await instance.database; + + // Format ke ISO 8601 untuk range, hasil: yyyy-MM-ddTHH:mm:ss + final startIso = start.toIso8601String(); + final endIso = end.toIso8601String(); + + final startDateYYYYMMDD = startIso.substring(0, 10); + final endDateYYYYMMDD = endIso.substring(0, 10); + + final List> 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> getOrderItemByOrderId(int orderId) async { final db = await instance.database; @@ -402,12 +427,12 @@ class ProductLocalDatasource { Future> getTableByStatus(String status) async { final db = await instance.database; List> 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..."); @@ -428,19 +453,19 @@ class ProductLocalDatasource { 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 _createDefaultTables() async { final db = await instance.database; - + // Create 5 default tables for (int i = 1; i <= 5; i++) { await db.insert(tableManagement, { @@ -463,7 +488,7 @@ class ProductLocalDatasource { 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, @@ -474,7 +499,7 @@ class ProductLocalDatasource { log("Verified table update: ${updatedTable.first}"); } } - + // Debug method to reset all tables to available status Future resetAllTablesToAvailable() async { log("Resetting all tables to available status..."); @@ -564,7 +589,7 @@ class ProductLocalDatasource { //update draft order Future updateDraftOrder(DraftOrderModel draftOrder) async { final db = await instance.database; - + // Update the draft order await db.update( 'draft_orders', @@ -572,13 +597,14 @@ class ProductLocalDatasource { 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!)); + await db.insert( + 'draft_order_items', orderItem.toMapForLocal(draftOrder.id!)); } } diff --git a/lib/presentation/home/pages/confirm_payment_page.dart b/lib/presentation/home/pages/confirm_payment_page.dart index 5f8863c..424ef94 100644 --- a/lib/presentation/home/pages/confirm_payment_page.dart +++ b/lib/presentation/home/pages/confirm_payment_page.dart @@ -32,10 +32,10 @@ class ConfirmPaymentPage extends StatefulWidget { final TableModel? table; const ConfirmPaymentPage({ - Key? key, + super.key, required this.isTable, this.table, - }) : super(key: key); + }); @override State createState() => _ConfirmPaymentPageState(); @@ -110,6 +110,7 @@ class _ConfirmPaymentPageState extends State { child: Hero( tag: 'payment_confirmation_screen', child: Scaffold( + backgroundColor: AppColors.white, body: Row( children: [ Expanded( @@ -401,7 +402,7 @@ class _ConfirmPaymentPageState extends State { final subTotal = price - (discount / 100 * price); final finalTax = subTotal * (tax / 100); - final finalDiscount = discount / 100 * subTotal; + // final finalDiscount = discount / 100 * subTotal; // discountAmountValue = finalDiscount.toInt(); // taxFinal = finalTax.toInt(); return Text( @@ -653,14 +654,19 @@ class _ConfirmPaymentPageState extends State { "🔘 Fetching available tables for Bayar Nanti"); isPayNow = false; isAddToOrder = false; - + // Debug: Check all tables first - final allTables = await ProductLocalDatasource.instance.getAllTable(); - print("🔘 All tables in database: ${allTables.length}"); + final allTables = + await ProductLocalDatasource + .instance + .getAllTable(); + print( + "🔘 All tables in database: ${allTables.length}"); allTables.forEach((table) { - print("🔘 Table: ${table.tableName} - Status: ${table.status} - ID: ${table.id}"); + print( + "🔘 Table: ${table.tableName} - Status: ${table.status} - ID: ${table.id}"); }); - + // Fetch available tables for Bayar Nanti context .read() @@ -683,14 +689,19 @@ class _ConfirmPaymentPageState extends State { "🔘 Fetching available tables for Bayar Nanti"); isPayNow = false; isAddToOrder = false; - + // Debug: Check all tables first - final allTables = await ProductLocalDatasource.instance.getAllTable(); - print("🔘 All tables in database: ${allTables.length}"); + final allTables = + await ProductLocalDatasource + .instance + .getAllTable(); + print( + "🔘 All tables in database: ${allTables.length}"); allTables.forEach((table) { - print("🔘 Table: ${table.tableName} - Status: ${table.status} - ID: ${table.id}"); + print( + "🔘 Table: ${table.tableName} - Status: ${table.status} - ID: ${table.id}"); }); - + // Fetch available tables for Bayar Nanti context .read() @@ -1516,8 +1527,8 @@ class _ConfirmPaymentPageState extends State { await ProductLocalDatasource .instance .getDraftOrderById( - selectTable! - .orderId ?? + selectTable + ?.orderId ?? 0); if (existingDraftOrder != diff --git a/lib/presentation/home/widgets/home_right_title.dart b/lib/presentation/home/widgets/home_right_title.dart index 29ee4ef..1319f82 100644 --- a/lib/presentation/home/widgets/home_right_title.dart +++ b/lib/presentation/home/widgets/home_right_title.dart @@ -4,6 +4,7 @@ import 'package:enaklo_pos/core/extensions/build_context_ext.dart'; import 'package:enaklo_pos/data/models/response/table_model.dart'; import 'package:enaklo_pos/presentation/home/dialog/type_dialog.dart'; import 'package:enaklo_pos/presentation/home/pages/dashboard_page.dart'; +import 'package:enaklo_pos/presentation/sales/pages/sales_page.dart'; import 'package:flutter/material.dart'; class HomeRightTitle extends StatelessWidget { @@ -33,7 +34,7 @@ class HomeRightTitle extends StatelessWidget { width: 180.0, height: 40, elevation: 0, - onPressed: () {}, + onPressed: () => context.push(SalesPage()), mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, icon: Icon( diff --git a/lib/presentation/sales/blocs/day_sales/day_sales_bloc.dart b/lib/presentation/sales/blocs/day_sales/day_sales_bloc.dart index d8f045b..a186f88 100644 --- a/lib/presentation/sales/blocs/day_sales/day_sales_bloc.dart +++ b/lib/presentation/sales/blocs/day_sales/day_sales_bloc.dart @@ -1,4 +1,3 @@ - import 'package:bloc/bloc.dart'; import 'package:enaklo_pos/data/datasources/product_local_datasource.dart'; import 'package:enaklo_pos/presentation/home/models/order_model.dart'; @@ -12,9 +11,14 @@ class DaySalesBloc extends Bloc { final ProductLocalDatasource datasource; DaySalesBloc(this.datasource) : super(const _Initial()) { on<_GetDaySales>((event, emit) async { + emit(const _Loading()); + final result = await datasource.getAllOrder(event.date); + emit(_Loaded(result)); + }); + on<_GetRangeDateSales>((event, emit) async { emit(const _Loading()); final result = - await datasource.getAllOrder(event.date); + await datasource.getAllOrderByRange(event.startDate, event.endDate); emit(_Loaded(result)); }); } diff --git a/lib/presentation/sales/blocs/day_sales/day_sales_bloc.freezed.dart b/lib/presentation/sales/blocs/day_sales/day_sales_bloc.freezed.dart index 75464d8..1081e35 100644 --- a/lib/presentation/sales/blocs/day_sales/day_sales_bloc.freezed.dart +++ b/lib/presentation/sales/blocs/day_sales/day_sales_bloc.freezed.dart @@ -20,18 +20,22 @@ mixin _$DaySalesEvent { TResult when({ required TResult Function() started, required TResult Function(DateTime date) getDaySales, + required TResult Function(DateTime startDate, DateTime endDate) + getRangeDateSales, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult? whenOrNull({ TResult? Function()? started, TResult? Function(DateTime date)? getDaySales, + TResult? Function(DateTime startDate, DateTime endDate)? getRangeDateSales, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult maybeWhen({ TResult Function()? started, TResult Function(DateTime date)? getDaySales, + TResult Function(DateTime startDate, DateTime endDate)? getRangeDateSales, required TResult orElse(), }) => throw _privateConstructorUsedError; @@ -39,18 +43,21 @@ mixin _$DaySalesEvent { TResult map({ required TResult Function(_Started value) started, required TResult Function(_GetDaySales value) getDaySales, + required TResult Function(_GetRangeDateSales value) getRangeDateSales, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult? mapOrNull({ TResult? Function(_Started value)? started, TResult? Function(_GetDaySales value)? getDaySales, + TResult? Function(_GetRangeDateSales value)? getRangeDateSales, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult maybeMap({ TResult Function(_Started value)? started, TResult Function(_GetDaySales value)? getDaySales, + TResult Function(_GetRangeDateSales value)? getRangeDateSales, required TResult orElse(), }) => throw _privateConstructorUsedError; @@ -120,6 +127,8 @@ class _$StartedImpl implements _Started { TResult when({ required TResult Function() started, required TResult Function(DateTime date) getDaySales, + required TResult Function(DateTime startDate, DateTime endDate) + getRangeDateSales, }) { return started(); } @@ -129,6 +138,7 @@ class _$StartedImpl implements _Started { TResult? whenOrNull({ TResult? Function()? started, TResult? Function(DateTime date)? getDaySales, + TResult? Function(DateTime startDate, DateTime endDate)? getRangeDateSales, }) { return started?.call(); } @@ -138,6 +148,7 @@ class _$StartedImpl implements _Started { TResult maybeWhen({ TResult Function()? started, TResult Function(DateTime date)? getDaySales, + TResult Function(DateTime startDate, DateTime endDate)? getRangeDateSales, required TResult orElse(), }) { if (started != null) { @@ -151,6 +162,7 @@ class _$StartedImpl implements _Started { TResult map({ required TResult Function(_Started value) started, required TResult Function(_GetDaySales value) getDaySales, + required TResult Function(_GetRangeDateSales value) getRangeDateSales, }) { return started(this); } @@ -160,6 +172,7 @@ class _$StartedImpl implements _Started { TResult? mapOrNull({ TResult? Function(_Started value)? started, TResult? Function(_GetDaySales value)? getDaySales, + TResult? Function(_GetRangeDateSales value)? getRangeDateSales, }) { return started?.call(this); } @@ -169,6 +182,7 @@ class _$StartedImpl implements _Started { TResult maybeMap({ TResult Function(_Started value)? started, TResult Function(_GetDaySales value)? getDaySales, + TResult Function(_GetRangeDateSales value)? getRangeDateSales, required TResult orElse(), }) { if (started != null) { @@ -252,6 +266,8 @@ class _$GetDaySalesImpl implements _GetDaySales { TResult when({ required TResult Function() started, required TResult Function(DateTime date) getDaySales, + required TResult Function(DateTime startDate, DateTime endDate) + getRangeDateSales, }) { return getDaySales(date); } @@ -261,6 +277,7 @@ class _$GetDaySalesImpl implements _GetDaySales { TResult? whenOrNull({ TResult? Function()? started, TResult? Function(DateTime date)? getDaySales, + TResult? Function(DateTime startDate, DateTime endDate)? getRangeDateSales, }) { return getDaySales?.call(date); } @@ -270,6 +287,7 @@ class _$GetDaySalesImpl implements _GetDaySales { TResult maybeWhen({ TResult Function()? started, TResult Function(DateTime date)? getDaySales, + TResult Function(DateTime startDate, DateTime endDate)? getRangeDateSales, required TResult orElse(), }) { if (getDaySales != null) { @@ -283,6 +301,7 @@ class _$GetDaySalesImpl implements _GetDaySales { TResult map({ required TResult Function(_Started value) started, required TResult Function(_GetDaySales value) getDaySales, + required TResult Function(_GetRangeDateSales value) getRangeDateSales, }) { return getDaySales(this); } @@ -292,6 +311,7 @@ class _$GetDaySalesImpl implements _GetDaySales { TResult? mapOrNull({ TResult? Function(_Started value)? started, TResult? Function(_GetDaySales value)? getDaySales, + TResult? Function(_GetRangeDateSales value)? getRangeDateSales, }) { return getDaySales?.call(this); } @@ -301,6 +321,7 @@ class _$GetDaySalesImpl implements _GetDaySales { TResult maybeMap({ TResult Function(_Started value)? started, TResult Function(_GetDaySales value)? getDaySales, + TResult Function(_GetRangeDateSales value)? getRangeDateSales, required TResult orElse(), }) { if (getDaySales != null) { @@ -322,6 +343,166 @@ abstract class _GetDaySales implements DaySalesEvent { throw _privateConstructorUsedError; } +/// @nodoc +abstract class _$$GetRangeDateSalesImplCopyWith<$Res> { + factory _$$GetRangeDateSalesImplCopyWith(_$GetRangeDateSalesImpl value, + $Res Function(_$GetRangeDateSalesImpl) then) = + __$$GetRangeDateSalesImplCopyWithImpl<$Res>; + @useResult + $Res call({DateTime startDate, DateTime endDate}); +} + +/// @nodoc +class __$$GetRangeDateSalesImplCopyWithImpl<$Res> + extends _$DaySalesEventCopyWithImpl<$Res, _$GetRangeDateSalesImpl> + implements _$$GetRangeDateSalesImplCopyWith<$Res> { + __$$GetRangeDateSalesImplCopyWithImpl(_$GetRangeDateSalesImpl _value, + $Res Function(_$GetRangeDateSalesImpl) _then) + : super(_value, _then); + + /// Create a copy of DaySalesEvent + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? startDate = null, + Object? endDate = null, + }) { + return _then(_$GetRangeDateSalesImpl( + null == startDate + ? _value.startDate + : startDate // ignore: cast_nullable_to_non_nullable + as DateTime, + null == endDate + ? _value.endDate + : endDate // ignore: cast_nullable_to_non_nullable + as DateTime, + )); + } +} + +/// @nodoc + +class _$GetRangeDateSalesImpl implements _GetRangeDateSales { + const _$GetRangeDateSalesImpl(this.startDate, this.endDate); + + @override + final DateTime startDate; + @override + final DateTime endDate; + + @override + String toString() { + return 'DaySalesEvent.getRangeDateSales(startDate: $startDate, endDate: $endDate)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$GetRangeDateSalesImpl && + (identical(other.startDate, startDate) || + other.startDate == startDate) && + (identical(other.endDate, endDate) || other.endDate == endDate)); + } + + @override + int get hashCode => Object.hash(runtimeType, startDate, endDate); + + /// Create a copy of DaySalesEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$GetRangeDateSalesImplCopyWith<_$GetRangeDateSalesImpl> get copyWith => + __$$GetRangeDateSalesImplCopyWithImpl<_$GetRangeDateSalesImpl>( + this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() started, + required TResult Function(DateTime date) getDaySales, + required TResult Function(DateTime startDate, DateTime endDate) + getRangeDateSales, + }) { + return getRangeDateSales(startDate, endDate); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? started, + TResult? Function(DateTime date)? getDaySales, + TResult? Function(DateTime startDate, DateTime endDate)? getRangeDateSales, + }) { + return getRangeDateSales?.call(startDate, endDate); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? started, + TResult Function(DateTime date)? getDaySales, + TResult Function(DateTime startDate, DateTime endDate)? getRangeDateSales, + required TResult orElse(), + }) { + if (getRangeDateSales != null) { + return getRangeDateSales(startDate, endDate); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Started value) started, + required TResult Function(_GetDaySales value) getDaySales, + required TResult Function(_GetRangeDateSales value) getRangeDateSales, + }) { + return getRangeDateSales(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Started value)? started, + TResult? Function(_GetDaySales value)? getDaySales, + TResult? Function(_GetRangeDateSales value)? getRangeDateSales, + }) { + return getRangeDateSales?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Started value)? started, + TResult Function(_GetDaySales value)? getDaySales, + TResult Function(_GetRangeDateSales value)? getRangeDateSales, + required TResult orElse(), + }) { + if (getRangeDateSales != null) { + return getRangeDateSales(this); + } + return orElse(); + } +} + +abstract class _GetRangeDateSales implements DaySalesEvent { + const factory _GetRangeDateSales( + final DateTime startDate, final DateTime endDate) = + _$GetRangeDateSalesImpl; + + DateTime get startDate; + DateTime get endDate; + + /// Create a copy of DaySalesEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + _$$GetRangeDateSalesImplCopyWith<_$GetRangeDateSalesImpl> get copyWith => + throw _privateConstructorUsedError; +} + /// @nodoc mixin _$DaySalesState { @optionalTypeArgs diff --git a/lib/presentation/sales/blocs/day_sales/day_sales_event.dart b/lib/presentation/sales/blocs/day_sales/day_sales_event.dart index 05b1184..b8889fd 100644 --- a/lib/presentation/sales/blocs/day_sales/day_sales_event.dart +++ b/lib/presentation/sales/blocs/day_sales/day_sales_event.dart @@ -6,4 +6,8 @@ class DaySalesEvent with _$DaySalesEvent { const factory DaySalesEvent.getDaySales( DateTime date, ) = _GetDaySales; + const factory DaySalesEvent.getRangeDateSales( + DateTime startDate, + DateTime endDate, + ) = _GetRangeDateSales; } diff --git a/lib/presentation/sales/dialog/filter_dialog.dart b/lib/presentation/sales/dialog/filter_dialog.dart new file mode 100644 index 0000000..91d74f3 --- /dev/null +++ b/lib/presentation/sales/dialog/filter_dialog.dart @@ -0,0 +1,123 @@ +import 'package:enaklo_pos/core/components/buttons.dart'; +import 'package:enaklo_pos/core/components/custom_modal_dialog.dart'; +import 'package:enaklo_pos/core/components/spaces.dart'; +import 'package:enaklo_pos/core/constants/colors.dart'; +import 'package:enaklo_pos/core/extensions/build_context_ext.dart'; +import 'package:enaklo_pos/core/extensions/date_time_ext.dart'; +import 'package:flutter/material.dart'; + +class SalesFilterDialog extends StatefulWidget { + final DateTime startDate; + final DateTime endDate; + final void Function(DateTime start, DateTime end) onDateRangeChanged; + const SalesFilterDialog( + {super.key, + required this.startDate, + required this.endDate, + required this.onDateRangeChanged}); + + @override + State createState() => _SalesFilterDialogState(); +} + +class _SalesFilterDialogState extends State { + late DateTimeRange selectedDateRange; + + @override + void initState() { + selectedDateRange = + DateTimeRange(start: widget.startDate, end: widget.endDate); + super.initState(); + } + + Future _selectDateRange(BuildContext context) async { + final DateTimeRange? picked = await showDateRangePicker( + context: context, + initialDateRange: selectedDateRange, + firstDate: DateTime(2020), + lastDate: DateTime(2100), + builder: (context, child) { + return Theme( + data: Theme.of(context).copyWith( + colorScheme: ColorScheme.light( + primary: Colors.blue, // Header color + onPrimary: Colors.white, // Header text color + onSurface: Colors.black, // Body text color + ), + ), + child: child!, + ); + }, + ); + + if (picked != null) { + setState(() { + selectedDateRange = picked; + }); + } + } + + @override + Widget build(BuildContext context) { + return CustomModalDialog( + title: 'Filter', + contentPadding: const EdgeInsets.all(16.0), + child: Column( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Periode', + style: TextStyle( + color: AppColors.black, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + GestureDetector( + onTap: () async => await _selectDateRange(context), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), + margin: EdgeInsets.only(top: 8), + decoration: BoxDecoration( + border: Border.all( + color: AppColors.primary, + ), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + Expanded( + child: Text( + '${selectedDateRange.start.toFormattedDate2()} - ${selectedDateRange.end.toFormattedDate2()}', + style: TextStyle( + color: AppColors.black, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ), + Icon( + Icons.calendar_month_outlined, + color: AppColors.primary, + ), + ], + ), + ), + ), + SpaceHeight(24), + Button.filled( + onPressed: () { + context.pop(); + widget.onDateRangeChanged( + selectedDateRange.start, selectedDateRange.end); + }, + label: 'Terapkan'), + ], + ), + ], + ), + ); + } +} diff --git a/lib/presentation/sales/pages/sales_page.dart b/lib/presentation/sales/pages/sales_page.dart index 30b14d5..b2d97d6 100644 --- a/lib/presentation/sales/pages/sales_page.dart +++ b/lib/presentation/sales/pages/sales_page.dart @@ -1,12 +1,18 @@ -import 'dart:developer'; - +import 'package:enaklo_pos/core/components/buttons.dart'; +import 'package:enaklo_pos/core/components/spaces.dart'; +import 'package:enaklo_pos/presentation/home/models/order_model.dart'; +import 'package:enaklo_pos/presentation/sales/blocs/day_sales/day_sales_bloc.dart'; +import 'package:enaklo_pos/presentation/sales/widgets/sales_detail.dart'; +import 'package:enaklo_pos/presentation/sales/widgets/sales_list_order.dart'; +import 'package:enaklo_pos/presentation/sales/widgets/sales_order_information.dart'; +import 'package:enaklo_pos/presentation/sales/widgets/sales_payment.dart'; +import 'package:enaklo_pos/presentation/sales/widgets/sales_right_title.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:enaklo_pos/core/constants/colors.dart'; -import 'package:enaklo_pos/core/extensions/date_time_ext.dart'; -import 'package:enaklo_pos/presentation/sales/blocs/day_sales/day_sales_bloc.dart'; -import '../widgets/sales_widget.dart'; +import '../../../core/constants/colors.dart'; +import '../widgets/sales_card.dart'; +import '../widgets/sales_title.dart'; class SalesPage extends StatefulWidget { const SalesPage({super.key}); @@ -16,110 +22,178 @@ class SalesPage extends StatefulWidget { } class _SalesPageState extends State { + DateTime startDate = DateTime.now(); + DateTime endDate = DateTime.now(); + OrderModel? orderDetail; + + int _total = 0; + String searchQuery = ''; + @override void initState() { - context.read().add(DaySalesEvent.getDaySales(DateTime.now())); + context + .read() + .add(DaySalesEvent.getRangeDateSales(startDate, endDate)); super.initState(); } + List _filterOrders(List orders) { + if (searchQuery.isEmpty) { + return orders; + } + + return orders.where((order) { + final customerName = order.customerName.toLowerCase(); + final queryLower = searchQuery.toLowerCase(); + return customerName.contains(queryLower); + }).toList(); + } + @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(32), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - 'Apskel POS ', - style: TextStyle( - color: AppColors.primary, - fontSize: 22, - fontWeight: FontWeight.w600, + return SafeArea( + child: Scaffold( + backgroundColor: AppColors.background, + body: Row( + children: [ + Expanded( + flex: 2, + child: Material( + color: AppColors.white, + child: Column( + children: [ + SalesTitle( + startDate: startDate, + endDate: endDate, + total: _total, + onChanged: (value) { + setState(() { + searchQuery = value; + }); + }, + onDateRangeChanged: (start, end) { + setState(() { + startDate = start; + endDate = end; + }); + + context.read().add( + DaySalesEvent.getRangeDateSales( + startDate, endDate)); + }, + ), + Expanded( + child: BlocBuilder( + builder: (context, state) { + return state.maybeWhen( + orElse: () => const Center( + child: CircularProgressIndicator(), + ), + loaded: (orders) { + final filtered = _filterOrders(orders); + if (filtered.isEmpty) { + return Center( + child: Text( + "Belum ada transaksi saat ini. ", + style: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.bold, + ), + ), + ); + } else { + WidgetsBinding.instance + .addPostFrameCallback((_) { + setState(() { + _total = filtered.length; + }); + }); + + return SingleChildScrollView( + child: Column( + children: List.generate( + filtered.length, + (index) => GestureDetector( + onTap: () { + setState(() { + orderDetail = filtered[index]; + }); + }, + child: SalesCard( + order: orders[index], + isActive: + orders[index] == orderDetail, + ), + ), + ), + ), + ); + } + }, + ); + }, + ), + ), + ], ), ), - Text( - "${DateTime.now().toFormattedDate()}", - style: const TextStyle( - color: AppColors.subtitle, - fontSize: 16, - ), - ), - ], - ), - const SizedBox( - height: 12.0, - ), - Expanded( - child: BlocBuilder( - builder: (context, state) { - return state.maybeWhen( - orElse: () => const Center( - child: CircularProgressIndicator(), - ), - loaded: (orders) { - log("message: ${orders.length}"); - if (orders.isEmpty) { - return Center( - child: Text( - "Belum ada transaksi saat ini. ", - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, - ), - ), - ); - } else { - return SalesWidget( - headerWidgets: _getTitleHeaderWidget(), - orders: orders, - ); - } - }, - ); - }, ), - ), - ], - ), - ); - } - - List _getTitleHeaderWidget() { - return [ - _getTitleItemWidget('ID', 40), - _getTitleItemWidget('Customer', 120), - _getTitleItemWidget('Status', 120), - _getTitleItemWidget('Sync', 60), - _getTitleItemWidget('Payment Status', 120), - _getTitleItemWidget('Payment Method', 120), - _getTitleItemWidget('Payment Amount', 120), - _getTitleItemWidget('Sub Total', 120), - _getTitleItemWidget('Tax', 120), - _getTitleItemWidget('Discount', 60), - _getTitleItemWidget('Service Charge', 120), - _getTitleItemWidget('Total', 120), - _getTitleItemWidget('Payment', 60), - _getTitleItemWidget('Item', 60), - _getTitleItemWidget('Cashier', 150), - _getTitleItemWidget('Time', 230), - _getTitleItemWidget('Action', 230), - ]; - } - - Widget _getTitleItemWidget(String label, double width) { - return Container( - width: width, - height: 56, - color: AppColors.primary, - alignment: Alignment.centerLeft, - child: Center( - child: Text( - label, - style: const TextStyle( - color: Colors.white, - ), + Expanded( + flex: 4, + child: orderDetail == null + ? Center( + child: Text( + "Belum ada order yang dipilih.", + style: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.bold, + ), + ), + ) + : Column( + children: [ + SalesRightTitle( + order: orderDetail, + actionWidget: [ + Button.outlined( + onPressed: () {}, + label: 'Refund', + icon: Icon(Icons.autorenew), + ), + ], + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + Row( + children: [ + Expanded( + child: SalesOrderInformation( + order: orderDetail, + ), + ), + SpaceWidth(16), + Expanded( + child: SalesDetail( + order: orderDetail, + ), + ), + ], + ), + SalesListOrder( + order: orderDetail, + ), + SalesPayment( + order: orderDetail, + ), + ], + ), + ) + ], + ), + ), + ], ), ), ); diff --git a/lib/presentation/sales/pages/sales_page.dart.backup b/lib/presentation/sales/pages/sales_page.dart.backup new file mode 100644 index 0000000..30b14d5 --- /dev/null +++ b/lib/presentation/sales/pages/sales_page.dart.backup @@ -0,0 +1,127 @@ +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:enaklo_pos/core/constants/colors.dart'; +import 'package:enaklo_pos/core/extensions/date_time_ext.dart'; +import 'package:enaklo_pos/presentation/sales/blocs/day_sales/day_sales_bloc.dart'; + +import '../widgets/sales_widget.dart'; + +class SalesPage extends StatefulWidget { + const SalesPage({super.key}); + + @override + State createState() => _SalesPageState(); +} + +class _SalesPageState extends State { + @override + void initState() { + context.read().add(DaySalesEvent.getDaySales(DateTime.now())); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(32), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Apskel POS ', + style: TextStyle( + color: AppColors.primary, + fontSize: 22, + fontWeight: FontWeight.w600, + ), + ), + Text( + "${DateTime.now().toFormattedDate()}", + style: const TextStyle( + color: AppColors.subtitle, + fontSize: 16, + ), + ), + ], + ), + const SizedBox( + height: 12.0, + ), + Expanded( + child: BlocBuilder( + builder: (context, state) { + return state.maybeWhen( + orElse: () => const Center( + child: CircularProgressIndicator(), + ), + loaded: (orders) { + log("message: ${orders.length}"); + if (orders.isEmpty) { + return Center( + child: Text( + "Belum ada transaksi saat ini. ", + style: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.bold, + ), + ), + ); + } else { + return SalesWidget( + headerWidgets: _getTitleHeaderWidget(), + orders: orders, + ); + } + }, + ); + }, + ), + ), + ], + ), + ); + } + + List _getTitleHeaderWidget() { + return [ + _getTitleItemWidget('ID', 40), + _getTitleItemWidget('Customer', 120), + _getTitleItemWidget('Status', 120), + _getTitleItemWidget('Sync', 60), + _getTitleItemWidget('Payment Status', 120), + _getTitleItemWidget('Payment Method', 120), + _getTitleItemWidget('Payment Amount', 120), + _getTitleItemWidget('Sub Total', 120), + _getTitleItemWidget('Tax', 120), + _getTitleItemWidget('Discount', 60), + _getTitleItemWidget('Service Charge', 120), + _getTitleItemWidget('Total', 120), + _getTitleItemWidget('Payment', 60), + _getTitleItemWidget('Item', 60), + _getTitleItemWidget('Cashier', 150), + _getTitleItemWidget('Time', 230), + _getTitleItemWidget('Action', 230), + ]; + } + + Widget _getTitleItemWidget(String label, double width) { + return Container( + width: width, + height: 56, + color: AppColors.primary, + alignment: Alignment.centerLeft, + child: Center( + child: Text( + label, + style: const TextStyle( + color: Colors.white, + ), + ), + ), + ); + } +} diff --git a/lib/presentation/sales/widgets/sales_card.dart b/lib/presentation/sales/widgets/sales_card.dart new file mode 100644 index 0000000..70dc6fb --- /dev/null +++ b/lib/presentation/sales/widgets/sales_card.dart @@ -0,0 +1,113 @@ +import 'package:enaklo_pos/core/constants/colors.dart'; +import 'package:enaklo_pos/core/extensions/date_time_ext.dart'; +import 'package:enaklo_pos/core/extensions/int_ext.dart'; +import 'package:enaklo_pos/presentation/home/models/order_model.dart'; +import 'package:flutter/material.dart'; + +class SalesCard extends StatelessWidget { + final OrderModel order; + final bool isActive; + + const SalesCard({ + super.key, + required this.order, + required this.isActive, + }); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + decoration: BoxDecoration( + color: isActive ? AppColors.primary.withOpacity(0.1) : AppColors.white, + border: + Border.all(color: isActive ? AppColors.primary : AppColors.stroke), + borderRadius: BorderRadius.circular(8), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CircleAvatar( + radius: 22, + backgroundColor: AppColors.primary, + child: Icon(Icons.person, color: Colors.white), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + order.customerName == "" + ? "Anonim" + : order.customerName, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 4), + Row( + children: [ + Icon(Icons.table_bar, size: 16, color: Colors.grey), + const SizedBox(width: 4), + Text( + 'Meja ${order.tableNumber}', + style: TextStyle(color: Colors.grey[600]), + ), + ], + ), + ], + ), + ), + Container( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.green.withOpacity(0.15), + borderRadius: BorderRadius.circular(16), + ), + child: Text( + order.status.toUpperCase(), + style: TextStyle( + color: Colors.green, + fontWeight: FontWeight.w600, + fontSize: 12, + letterSpacing: 0.5, + ), + ), + ), + ], + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + order.total.currencyFormatRpV2, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: AppColors.primary, + ), + ), + Text( + DateTime.parse(order.transactionTime).toFormattedDate2(), + style: TextStyle( + color: AppColors.black, + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/sales/widgets/sales_detail.dart b/lib/presentation/sales/widgets/sales_detail.dart new file mode 100644 index 0000000..77549de --- /dev/null +++ b/lib/presentation/sales/widgets/sales_detail.dart @@ -0,0 +1,76 @@ +import 'package:enaklo_pos/core/constants/colors.dart'; +import 'package:enaklo_pos/core/extensions/date_time_ext.dart'; +import 'package:enaklo_pos/presentation/home/models/order_model.dart'; +import 'package:flutter/material.dart'; + +class SalesDetail extends StatelessWidget { + final OrderModel? order; + const SalesDetail({super.key, this.order}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.white, + borderRadius: BorderRadius.circular(8), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Detail', + style: TextStyle( + color: AppColors.black, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + _item( + title: 'Pelanggan', + value: order?.customerName ?? "-", + ), + _item( + title: 'Waktu', + value: + DateTime.parse(order?.transactionTime ?? "").toFormattedDate3(), + ), + _item( + title: 'Status', + value: order?.paymentStatus ?? "-", + ), + _item( + title: 'Jenis Order', + value: order?.paymentMethod ?? "-", + ), + ], + ), + ); + } + + Padding _item({ + required String title, + required String value, + }) { + return Padding( + padding: const EdgeInsets.only(top: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 14, + ), + ), + Text( + value, + style: const TextStyle( + fontSize: 14, + ), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/sales/widgets/sales_list_order.dart b/lib/presentation/sales/widgets/sales_list_order.dart new file mode 100644 index 0000000..b8ff4ec --- /dev/null +++ b/lib/presentation/sales/widgets/sales_list_order.dart @@ -0,0 +1,88 @@ +import 'package:enaklo_pos/core/constants/colors.dart'; +import 'package:enaklo_pos/presentation/home/models/order_model.dart'; +import 'package:enaklo_pos/presentation/home/models/product_quantity.dart'; +import 'package:flutter/material.dart'; + +class SalesListOrder extends StatelessWidget { + final OrderModel? order; + const SalesListOrder({super.key, this.order}); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.only(top: 16), + decoration: BoxDecoration( + color: AppColors.white, + borderRadius: BorderRadius.circular(8), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(16), + width: double.infinity, + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide(color: AppColors.background), + ), + ), + child: Text( + 'Daftar Pembelian', + style: TextStyle( + color: AppColors.black, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + Column( + children: List.generate( + order?.orderItems.length ?? 0, + (index) => _item(order!.orderItems[index]), + ).toList(), + ), + ], + ), + ); + } + + Padding _item(ProductQuantity product) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16) + .copyWith(bottom: 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + children: [ + Text( + product.product.name ?? '', + style: const TextStyle( + fontSize: 14, + ), + ), + Text( + product.product.price ?? '', + style: const TextStyle( + fontSize: 14, + ), + ), + ], + ), + Text( + 'X${product.quantity}', + style: const TextStyle( + fontSize: 14, + ), + ), + Text( + product.product.price ?? '', + style: const TextStyle( + fontSize: 14, + ), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/sales/widgets/sales_order_information.dart b/lib/presentation/sales/widgets/sales_order_information.dart new file mode 100644 index 0000000..88ccece --- /dev/null +++ b/lib/presentation/sales/widgets/sales_order_information.dart @@ -0,0 +1,76 @@ +import 'package:enaklo_pos/core/constants/colors.dart'; +import 'package:enaklo_pos/core/extensions/date_time_ext.dart'; +import 'package:enaklo_pos/presentation/home/models/order_model.dart'; +import 'package:flutter/material.dart'; + +class SalesOrderInformation extends StatelessWidget { + final OrderModel? order; + const SalesOrderInformation({super.key, this.order}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.white, + borderRadius: BorderRadius.circular(8), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Informasi Pesanan', + style: TextStyle( + color: AppColors.black, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + _item( + title: 'No. Order', + value: "${order?.id}", + ), + _item( + title: 'Tanggal', + value: + DateTime.parse(order?.transactionTime ?? "").toFormattedDate3(), + ), + _item( + title: 'Kasir', + value: order?.namaKasir ?? "-", + ), + _item( + title: 'Jenis Order', + value: order?.orderType.value ?? "-", + ), + ], + ), + ); + } + + Padding _item({ + required String title, + required String value, + }) { + return Padding( + padding: const EdgeInsets.only(top: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 14, + ), + ), + Text( + value, + style: const TextStyle( + fontSize: 14, + ), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/sales/widgets/sales_payment.dart b/lib/presentation/sales/widgets/sales_payment.dart new file mode 100644 index 0000000..5998841 --- /dev/null +++ b/lib/presentation/sales/widgets/sales_payment.dart @@ -0,0 +1,99 @@ +import 'package:enaklo_pos/core/components/dashed_divider.dart'; +import 'package:enaklo_pos/core/components/spaces.dart'; +import 'package:enaklo_pos/core/constants/colors.dart'; +import 'package:enaklo_pos/core/extensions/int_ext.dart'; +import 'package:enaklo_pos/presentation/home/models/order_model.dart'; +import 'package:flutter/material.dart'; + +class SalesPayment extends StatelessWidget { + final OrderModel? order; + const SalesPayment({super.key, this.order}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(16), + margin: const EdgeInsets.only(top: 16), + decoration: BoxDecoration( + color: AppColors.white, + borderRadius: BorderRadius.circular(8), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Informasi Pesanan', + style: TextStyle( + color: AppColors.black, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + SpaceHeight(12), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Subtotal ${order?.totalItem} Produk', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + Text( + (order?.subTotal)?.currencyFormatRp ?? "0", + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + SpaceHeight(12), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Pajak (11%)', + style: const TextStyle( + fontSize: 16, + ), + ), + Text( + (order?.tax)?.currencyFormatRp ?? "0", + style: const TextStyle( + fontSize: 16, + ), + ), + ], + ), + SpaceHeight(12), + DashedDivider( + color: AppColors.stroke, + ), + SpaceHeight(12), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Total', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + ), + ), + Text( + (order?.total)?.currencyFormatRp ?? "0", + style: const TextStyle( + color: AppColors.primary, + fontSize: 18, + fontWeight: FontWeight.w700, + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/presentation/sales/widgets/sales_right_title.dart b/lib/presentation/sales/widgets/sales_right_title.dart new file mode 100644 index 0000000..2d56531 --- /dev/null +++ b/lib/presentation/sales/widgets/sales_right_title.dart @@ -0,0 +1,43 @@ +import 'package:enaklo_pos/core/constants/colors.dart'; +import 'package:enaklo_pos/core/extensions/build_context_ext.dart'; +import 'package:enaklo_pos/presentation/home/models/order_model.dart'; +import 'package:flutter/material.dart'; + +class SalesRightTitle extends StatelessWidget { + final OrderModel? order; + final List? actionWidget; + const SalesRightTitle({super.key, this.order, this.actionWidget}); + + @override + Widget build(BuildContext context) { + return Container( + height: context.deviceHeight * 0.1, + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), + decoration: BoxDecoration( + color: AppColors.white, + border: Border( + bottom: BorderSide( + color: AppColors.background, + width: 1.0, + ), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + "Detail Pesanan #${order?.id}", + style: TextStyle( + color: AppColors.black, + fontSize: 20, + fontWeight: FontWeight.w600, + ), + ), + ), + if (actionWidget != null) ...actionWidget!, + ], + ), + ); + } +} diff --git a/lib/presentation/sales/widgets/sales_title.dart b/lib/presentation/sales/widgets/sales_title.dart new file mode 100644 index 0000000..83b699c --- /dev/null +++ b/lib/presentation/sales/widgets/sales_title.dart @@ -0,0 +1,146 @@ +import 'package:enaklo_pos/core/components/components.dart'; +import 'package:enaklo_pos/core/constants/colors.dart'; +import 'package:enaklo_pos/core/extensions/build_context_ext.dart'; +import 'package:enaklo_pos/core/extensions/date_time_ext.dart'; +import 'package:enaklo_pos/presentation/sales/dialog/filter_dialog.dart'; +import 'package:flutter/material.dart'; + +class SalesTitle extends StatelessWidget { + final DateTime startDate; + final DateTime endDate; + final int total; + final Function(String) onChanged; + final void Function(DateTime start, DateTime end) onDateRangeChanged; + + const SalesTitle( + {super.key, + required this.startDate, + required this.endDate, + required this.onChanged, + required this.total, + required this.onDateRangeChanged}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Container( + height: context.deviceHeight * 0.1, + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), + decoration: BoxDecoration( + color: AppColors.white, + border: Border( + bottom: BorderSide( + color: AppColors.background, + width: 1.0, + ), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + GestureDetector( + onTap: () => context.pop(), + child: Icon( + Icons.arrow_back, + color: AppColors.primary, + size: 24, + ), + ), + SpaceWidth(16), + Expanded( + child: Text( + "Daftar Pesanan", + style: TextStyle( + color: AppColors.black, + fontSize: 20, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 12.0, + ), + decoration: BoxDecoration( + color: AppColors.white, + border: Border( + bottom: BorderSide( + color: AppColors.background, + width: 1.0, + ), + ), + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + startDate.toFormattedDate2() == endDate.toFormattedDate2() + ? startDate.toFormattedDate2() + : '${startDate.toFormattedDate2()} - ${endDate.toFormattedDate2()}', + style: TextStyle( + color: AppColors.black, + fontWeight: FontWeight.w600, + ), + ), + Text( + '$total Pesanan', + style: TextStyle( + color: AppColors.black, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + SpaceHeight(16), + Row( + children: [ + Expanded( + child: TextFormField( + onChanged: onChanged, + decoration: InputDecoration( + prefixIcon: Icon( + Icons.search, + ), + hintText: 'Cari Pesanan', + ), + ), + ), + SpaceWidth(12), + GestureDetector( + onTap: () => showDialog( + context: context, + builder: (context) => SalesFilterDialog( + startDate: startDate, + endDate: endDate, + onDateRangeChanged: onDateRangeChanged, + ), + ), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColors.primary, + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + Icons.filter_list_outlined, + color: AppColors.white, + size: 24, + ), + ), + ), + ], + ), + ], + ), + ), + ], + ); + } +}