From 5af0259f0ad4c9f4a70085f4c331f2c9e43c7490 Mon Sep 17 00:00:00 2001 From: efrilm Date: Tue, 5 Aug 2025 12:34:41 +0700 Subject: [PATCH] feat: table page --- lib/data/models/response/table_model.dart | 24 +- .../home/pages/dashboard_page.dart | 4 +- lib/presentation/table/pages/table_page.dart | 443 ++++++++++++--- .../table/pages/table_page.dart.backup | 95 ++++ .../table/widgets/table_widget.dart | 511 +++++------------- .../table/widgets/table_widget.dart.backup | 399 ++++++++++++++ 6 files changed, 1021 insertions(+), 455 deletions(-) create mode 100644 lib/presentation/table/pages/table_page.dart.backup create mode 100644 lib/presentation/table/widgets/table_widget.dart.backup diff --git a/lib/data/models/response/table_model.dart b/lib/data/models/response/table_model.dart index 29972c4..01291ce 100644 --- a/lib/data/models/response/table_model.dart +++ b/lib/data/models/response/table_model.dart @@ -68,18 +68,18 @@ class TableData { } class TableModel { - final String? id; - final String? organizationId; - final String? outletId; - final String? tableName; - final String? status; - final int? paymentAmount; - final double? positionX; - final double? positionY; - final int? capacity; - final bool? isActive; - final DateTime? createdAt; - final DateTime? updatedAt; + String? id; + String? organizationId; + String? outletId; + String? tableName; + String? status; + int? paymentAmount; + double? positionX; + double? positionY; + int? capacity; + bool? isActive; + DateTime? createdAt; + DateTime? updatedAt; TableModel({ this.id, diff --git a/lib/presentation/home/pages/dashboard_page.dart b/lib/presentation/home/pages/dashboard_page.dart index 29cb86b..92ba892 100644 --- a/lib/presentation/home/pages/dashboard_page.dart +++ b/lib/presentation/home/pages/dashboard_page.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'dart:developer'; import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:enaklo_pos/presentation/table/pages/table_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:enaklo_pos/data/models/response/table_model.dart'; @@ -57,8 +58,7 @@ class _DashboardPageState extends State { isTable: false, table: widget.table, ), - // const TablePage(), - TableManagementScreen(), + const TablePage(), const ReportPage(), const PrinterConfigurationPage(), // SalesPage(), diff --git a/lib/presentation/table/pages/table_page.dart b/lib/presentation/table/pages/table_page.dart index 570d051..5b424ca 100644 --- a/lib/presentation/table/pages/table_page.dart +++ b/lib/presentation/table/pages/table_page.dart @@ -1,10 +1,32 @@ -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter/material.dart'; -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/data/models/response/table_model.dart'; +import 'package:enaklo_pos/presentation/home/pages/dashboard_page.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/get_table/get_table_bloc.dart'; -import 'package:enaklo_pos/presentation/table/dialogs/form_table_dialog.dart'; -import 'package:enaklo_pos/presentation/table/widgets/card_table_widget.dart'; +import 'package:enaklo_pos/presentation/table/dialogs/form_table_new_dialog.dart'; +import 'package:enaklo_pos/presentation/table/widgets/table_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +// Enum status meja +enum TableStatus { available, occupied, billed, availableSoon, unknown } + +TableStatus parseStatus(String? status) { + switch (status) { + case 'available': + return TableStatus.available; + case 'occupied': + return TableStatus.occupied; + case 'billed': + return TableStatus.billed; + case 'available_soon': + return TableStatus.availableSoon; + default: + return TableStatus.unknown; + } +} class TablePage extends StatefulWidget { const TablePage({super.key}); @@ -14,6 +36,23 @@ class TablePage extends StatefulWidget { } class _TablePageState extends State { + TableModel? selectedTable; + + // Untuk drag + TableModel? draggingTable; + + // Ubah function toggleSelectTable menjadi selectTable + void selectTable(TableModel table) { + setState(() { + if (selectedTable == table) { + selectedTable = null; // Deselect jika table yang sama diklik + } else { + selectedTable = + table; // Select table baru (akan mengganti selection sebelumnya) + } + }); + } + @override void initState() { context.read().add(const GetTableEvent.getTables()); @@ -22,74 +61,340 @@ class _TablePageState extends State { @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(24), - child: ListView( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Table Management", - style: TextStyle( - fontSize: 24.0, - fontWeight: FontWeight.bold, - color: AppColors.primary, - ), - ), - Button.filled( - onPressed: () { - showDialog( - context: context, - builder: (context) => FormTableDialog(), - ); - }, - label: 'Generate Table', - height: 48.0, - width: 200.0, - ), - ], - ), - SpaceHeight(24.0), - BlocBuilder( - builder: (context, state) { - return state.maybeWhen( - orElse: () { - return SizedBox.shrink(); - }, - loading: () { - return const CircularProgressIndicator(); - }, - success: (tables) { - if (tables.isEmpty) { - return const Center( - child: Text('No table available'), - ); - } - return GridView.builder( - padding: EdgeInsets.zero, - gridDelegate: - const SliverGridDelegateWithFixedCrossAxisCount( - childAspectRatio: 1.0, - crossAxisCount: 4, - mainAxisSpacing: 16, - crossAxisSpacing: 16, - ), - itemCount: tables.length, - shrinkWrap: true, - physics: const ScrollPhysics(), - itemBuilder: (BuildContext context, int index) { - return CardTableWidget( - table: tables[index], - ); - }, - ); - }, - ); + final double mapWidth = context.deviceWidth * 2; + final double mapHeight = context.deviceHeight * 1.5; + + return Scaffold( + appBar: AppBar( + title: const Text("Layout Meja"), + actions: [ + BlocListener( + listener: (context, state) { + state.maybeWhen( + orElse: () {}, + success: (message) { + context + .read() + .add(const GetTableEvent.getTables()); + }); }, + child: IconButton( + icon: const Icon(Icons.add), + onPressed: () { + showDialog( + context: context, + builder: (context) => FormTableNewDialog(), + ); + }, + ), ), ], + backgroundColor: Colors.white, + foregroundColor: Colors.black, + elevation: 0.5, + bottom: PreferredSize( + preferredSize: const Size.fromHeight(60), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + children: [ + _buildLegendDot(Colors.blue[200]!, "Available"), + const SizedBox(width: 16), + _buildLegendDot(Colors.orange[200]!, "Occupied"), + const SizedBox(width: 16), + _buildLegendDot(Colors.green[200]!, "Billed"), + const SizedBox(width: 16), + _buildLegendDot(Colors.yellow[200]!, "Available soon"), + ], + ), + ), + ), + ), + backgroundColor: const Color(0xFFF7F8FA), + body: BlocBuilder( + builder: (context, state) { + return state.maybeWhen( + orElse: () => SizedBox.shrink(), + success: (tables) => SafeArea( + child: Stack( + children: [ + // Main content area + Row( + children: [ + // Area meja (zoom & pan & drag) + Expanded( + flex: 5, + child: InteractiveViewer( + panEnabled: true, + scaleEnabled: true, + constrained: false, + boundaryMargin: const EdgeInsets.all(80), + minScale: 0.3, + maxScale: 3.0, + alignment: Alignment.topLeft, + child: Container( + width: mapWidth, + height: mapHeight, + decoration: BoxDecoration( + color: const Color(0xFFF7F8FA), + border: Border.all( + color: Colors.grey[300]!, width: 2), + ), + child: Stack( + children: [ + // Optional: Grid background + ...List.generate( + 20, + (i) => Positioned( + left: i * 100.0, + top: 0, + bottom: 0, + child: Container( + width: 1, + color: Colors.grey[200]), + )), + ...List.generate( + 15, + (i) => Positioned( + top: i * 100.0, + left: 0, + right: 0, + child: Container( + height: 1, + color: Colors.grey[200]), + )), + + // Tables + ...tables.map((table) { + final isSelected = selectedTable == table; + return Positioned( + left: table.positionX, + top: table.positionY, + child: Draggable( + data: table, + feedback: Material( + color: Colors.transparent, + child: TableWidget( + table: table, + isSelected: isSelected, + ), + ), + childWhenDragging: Opacity( + opacity: 0.5, + child: TableWidget( + table: table, + isSelected: isSelected, + ), + ), + onDragStarted: () { + setState(() { + draggingTable = table; + }); + }, + onDraggableCanceled: (velocity, offset) { + setState(() { + draggingTable = null; + }); + }, + onDragEnd: (details) { + setState(() { + draggingTable = null; + final RenderBox box = context + .findRenderObject() as RenderBox; + final Offset local = + box.globalToLocal(details.offset); + table.positionX = + local.dx.clamp(0, mapWidth - 120); + table.positionY = + local.dy.clamp(0, mapHeight - 80); + + context + .read() + .add(ChangePositionTableEvent + .changePositionTable( + tableId: table.id ?? "", + position: details.offset, + )); + }); + }, + child: GestureDetector( + onTap: () => selectTable(table), + child: TableWidget( + table: table, + isSelected: isSelected, + ), + ), + ), + ); + }).toList(), + ], + ), + ), + ), + ), + // Sidebar bar tables + ], + ), + + // Floating bottom bar - hanya muncul jika ada table yang dipilih + buildAlternativeFloatingBar(), + ], + ), + ), + ); + }, ), ); } + + Widget buildAlternativeFloatingBar() { + return Positioned( + bottom: 20, // Jarak dari bawah + left: 16, // Jarak dari kiri + right: 16, + child: AnimatedSlide( + duration: const Duration(milliseconds: 400), + curve: Curves.elasticOut, + offset: selectedTable == null ? const Offset(0, 2) : Offset.zero, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 200), + opacity: selectedTable == null ? 0.0 : 1.0, + child: IgnorePointer( + ignoring: selectedTable == null, + child: Container( + margin: const EdgeInsets.all(16), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppColors.primary, + AppColors.primary.withOpacity(0.6) + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: AppColors.primary.withOpacity(0.3), + blurRadius: 20, + offset: const Offset(0, 8), + ), + ], + ), + child: Row( + children: [ + const Icon( + Icons.table_bar, + color: Colors.white, + size: 24, + ), + const SizedBox(width: 12), + Text( + "1 Meja Dipilih", + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + const SizedBox(width: 16), + Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row(children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(16), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + selectedTable?.tableName ?? "", + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(width: 4), + GestureDetector( + onTap: () { + setState(() { + selectedTable = null; + }); + }, + child: const Icon( + Icons.close, + color: Colors.white, + size: 16, + ), + ), + ], + ), + ), + ), + ]), + ), + ), + const SizedBox(width: 16), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: AppColors.primary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + ), + onPressed: () { + if (selectedTable?.status == 'available') { + context.push(DashboardPage( + table: selectedTable!, + )); + } else { + // Handle occupied table click - load draft order and navigate to payment + // context.read().add( + // CheckoutEvent.loadDraftOrder(data!), + // ); + // log("Data Draft Order: ${data!.toMap()}"); + // context.push(PaymentTablePage( + // table: widget.table, + // draftOrder: data!, + // )); + } + }, + child: const Text( + "Tempatkan Pesanan", + style: TextStyle(fontWeight: FontWeight.bold), + ), + ), + ], + ), + ), + ), + ), + ), + ); + } + + Widget _buildLegendDot(Color color, String label) { + return Row( + children: [ + CircleAvatar(radius: 7, backgroundColor: color), + const SizedBox(width: 6), + Text(label, style: const TextStyle(fontSize: 14)), + ], + ); + } } diff --git a/lib/presentation/table/pages/table_page.dart.backup b/lib/presentation/table/pages/table_page.dart.backup new file mode 100644 index 0000000..570d051 --- /dev/null +++ b/lib/presentation/table/pages/table_page.dart.backup @@ -0,0 +1,95 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter/material.dart'; +import 'package:enaklo_pos/core/components/components.dart'; +import 'package:enaklo_pos/core/constants/colors.dart'; +import 'package:enaklo_pos/presentation/table/blocs/get_table/get_table_bloc.dart'; +import 'package:enaklo_pos/presentation/table/dialogs/form_table_dialog.dart'; +import 'package:enaklo_pos/presentation/table/widgets/card_table_widget.dart'; + +class TablePage extends StatefulWidget { + const TablePage({super.key}); + + @override + State createState() => _TablePageState(); +} + +class _TablePageState extends State { + @override + void initState() { + context.read().add(const GetTableEvent.getTables()); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(24), + child: ListView( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Table Management", + style: TextStyle( + fontSize: 24.0, + fontWeight: FontWeight.bold, + color: AppColors.primary, + ), + ), + Button.filled( + onPressed: () { + showDialog( + context: context, + builder: (context) => FormTableDialog(), + ); + }, + label: 'Generate Table', + height: 48.0, + width: 200.0, + ), + ], + ), + SpaceHeight(24.0), + BlocBuilder( + builder: (context, state) { + return state.maybeWhen( + orElse: () { + return SizedBox.shrink(); + }, + loading: () { + return const CircularProgressIndicator(); + }, + success: (tables) { + if (tables.isEmpty) { + return const Center( + child: Text('No table available'), + ); + } + return GridView.builder( + padding: EdgeInsets.zero, + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + childAspectRatio: 1.0, + crossAxisCount: 4, + mainAxisSpacing: 16, + crossAxisSpacing: 16, + ), + itemCount: tables.length, + shrinkWrap: true, + physics: const ScrollPhysics(), + itemBuilder: (BuildContext context, int index) { + return CardTableWidget( + table: tables[index], + ); + }, + ); + }, + ); + }, + ), + ], + ), + ); + } +} diff --git a/lib/presentation/table/widgets/table_widget.dart b/lib/presentation/table/widgets/table_widget.dart index d9e5441..1b41269 100644 --- a/lib/presentation/table/widgets/table_widget.dart +++ b/lib/presentation/table/widgets/table_widget.dart @@ -1,395 +1,162 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first -import 'dart:developer'; - -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:enaklo_pos/core/components/buttons.dart'; -import 'package:enaklo_pos/core/components/custom_text_field.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/utils/date_formatter.dart'; -import 'package:enaklo_pos/data/datasources/product_local_datasource.dart'; - import 'package:enaklo_pos/data/models/response/table_model.dart'; -import 'package:enaklo_pos/presentation/home/bloc/checkout/checkout_bloc.dart'; -import 'package:enaklo_pos/presentation/home/bloc/status_table/status_table_bloc.dart'; -import 'package:enaklo_pos/presentation/home/pages/dashboard_page.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/update_table/update_table_bloc.dart'; -import 'package:enaklo_pos/presentation/table/models/draft_order_model.dart'; +import 'package:enaklo_pos/presentation/table/pages/table_page.dart'; +import 'package:flutter/material.dart'; -import '../pages/payment_table_page.dart'; - -class TableWidget extends StatefulWidget { +class TableWidget extends StatelessWidget { final TableModel table; - const TableWidget({ - super.key, - required this.table, - }); + final bool isSelected; - @override - State createState() => _TableWidgetState(); -} + const TableWidget({super.key, required this.table, this.isSelected = false}); -class _TableWidgetState extends State { - TextEditingController? tableNameController; - DraftOrderModel? data; - @override - void initState() { - super.initState(); - loadData(); - tableNameController = TextEditingController(text: widget.table.tableName); + // Fungsi untuk menentukan jumlah kursi di tiap sisi + Map getChairDistribution(int capacity) { + if (capacity == 2) { + return {'top': 0, 'bottom': 0, 'left': 1, 'right': 1}; + } else if (capacity == 4) { + return {'top': 1, 'bottom': 1, 'left': 1, 'right': 1}; + } else if (capacity == 6) { + return {'top': 2, 'bottom': 2, 'left': 1, 'right': 1}; + } else if (capacity == 8) { + return {'top': 3, 'bottom': 3, 'left': 1, 'right': 1}; + } else if (capacity == 10) { + return {'top': 4, 'bottom': 4, 'left': 1, 'right': 1}; + } else { + int side = (capacity / 4).floor(); + return {'top': side, 'bottom': side, 'left': 1, 'right': 1}; + } } - @override - void dispose() { - tableNameController!.dispose(); - super.dispose(); + Color getStatusColor() { + switch (parseStatus(table.status)) { + case TableStatus.available: + return Colors.blue[100]!; + case TableStatus.occupied: + return Colors.orange[100]!; + case TableStatus.billed: + return Colors.green[100]!; + case TableStatus.availableSoon: + return Colors.yellow[100]!; + default: + return Colors.grey[200]!; + } } - loadData() async { - if (widget.table.status != 'available') { - // data = await ProductLocalDatasource.instance - // .getDraftOrderById(widget.table.orderId); + Color getBorderColor() { + if (isSelected) return AppColors.primary; + switch (parseStatus(table.status)) { + case TableStatus.available: + return Colors.blue; + case TableStatus.occupied: + return Colors.orange; + case TableStatus.billed: + return Colors.green; + case TableStatus.availableSoon: + return Colors.yellow[700]!; + default: + return Colors.grey; } } @override Widget build(BuildContext context) { - return GestureDetector( - onTap: () async { - if (widget.table.status == 'available') { - context.push(DashboardPage( - table: widget.table, - )); - } else { - // Handle occupied table click - load draft order and navigate to payment - context.read().add( - CheckoutEvent.loadDraftOrder(data!), - ); - log("Data Draft Order: ${data!.toMap()}"); - context.push(PaymentTablePage( - table: widget.table, - draftOrder: data!, - )); - } - }, - onLongPress: () { - // dialog info table - showDialog( - context: context, - builder: (context) { - return AlertDialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16)), - title: Row( - children: [ - Icon(Icons.table_bar, color: AppColors.primary), - SizedBox(width: 8), - Text('Table ${widget.table.tableName}'), - Spacer(), - BlocListener( - listener: (context, state) { - state.maybeWhen( - orElse: () {}, - success: (message) { - context - .read() - .add(const GetTableEvent.getTables()); - context.pop(); - }); - }, - child: IconButton( - onPressed: () { - // show dialaog adn input table name - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: Text('Update Table'), - content: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - maxHeight: 180, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - CustomTextField( - controller: tableNameController!, - label: 'Table Name', - ), - SpaceHeight(16), - Row( - children: [ - Expanded( - child: Button.outlined( - onPressed: () { - context.pop(); - }, - label: 'close', - ), - ), - SpaceWidth(16), - Expanded( - child: Button.filled( - onPressed: () { - // final newData = - // TableModel( - // id: widget.table.id, - // tableName: - // tableNameController! - // .text, - // status: - // widget.table.status, - // startTime: widget - // .table.startTime, - // orderId: widget - // .table.orderId, - // paymentAmount: widget - // .table - // .paymentAmount, - // position: widget - // .table.position, - // ); - // context - // .read< - // UpdateTableBloc>() - // .add( - // UpdateTableEvent - // .updateTable( - // newData, - // ), - // ); - context - .pop(); // close dialog after adding - }, - label: 'Update', - ), - ) - ], - ) - ], - ), - ), - ), - actions: []); - }); - }, - icon: Icon(Icons.edit)), - ), - ], - ), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildInfoRow( - 'Status:', - widget.table.status == 'available' - ? 'Available' - : 'Occupied', - color: widget.table.status == 'available' - ? Colors.green - : Colors.red), - // widget.table.status == 'available' - // ? SizedBox.shrink() - // : _buildInfoRow( - // 'Start Time:', - // DateFormatter.formatDateTime2( - // widget.table.startTime)), - // widget.table.status == 'available' - // ? SizedBox.shrink() - // : _buildInfoRow( - // 'Order ID:', widget.table.orderId.toString()), - widget.table.status == 'available' - ? SizedBox.shrink() - : SpaceHeight(16), - widget.table.status == 'available' - ? SizedBox.shrink() - : Row( - children: [ - Expanded( - child: Button.outlined( - onPressed: () { - // Show void confirmation dialog - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Row( - children: [ - Icon(Icons.warning, - color: AppColors.red), - SizedBox(width: 8), - Text('Void Order?'), - ], - ), - content: Text( - 'Apakah anda yakin ingin membatalkan pesanan untuk meja ${widget.table.tableName}?\n\nPesanan akan dihapus secara permanen.'), - actions: [ - TextButton( - onPressed: () => - Navigator.pop(context), - child: Text('Tidak', - style: TextStyle( - color: AppColors.primary)), - ), - BlocListener( - listener: (context, state) { - state.maybeWhen( - orElse: () {}, - success: () { - context - .read() - .add(const GetTableEvent - .getTables()); - Navigator.pop( - context); // Close void dialog - Navigator.pop( - context); // Close table info dialog - ScaffoldMessenger.of(context) - .showSnackBar( - const SnackBar( - content: Text( - 'Pesanan berhasil dibatalkan'), - backgroundColor: - AppColors.primary, - ), - ); - }, - ); - }, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.red, - ), - onPressed: () { - // // Void the order - // final newTable = TableModel( - // id: widget.table.id, - // tableName: - // widget.table.tableName, - // status: 'available', - // orderId: 0, - // paymentAmount: 0, - // startTime: DateTime.now() - // .toIso8601String(), - // position: widget.table.position, - // ); - // context - // .read() - // .add( - // StatusTableEvent - // .statusTabel(newTable), - // ); - // // Remove draft order from local storage - // ProductLocalDatasource.instance - // .removeDraftOrderById( - // widget.table.orderId); - // log("Voided order for table: ${widget.table.tableName}"); - }, - child: const Text( - "Ya, Batalkan", - style: TextStyle( - color: Colors.white), - ), - ), - ), - ], - ), - ); - }, - label: 'Void Order', - color: AppColors.red, - textColor: AppColors.red, - ), - ), - SizedBox(width: 12), - Expanded( - child: BlocConsumer( - listener: (context, state) { - state.maybeWhen( - orElse: () {}, - success: () { - context.read().add( - const GetTableEvent.getTables()); - context.pop(); - }); - }, - builder: (context, state) { - return Button.filled( - onPressed: () { - context.pop(); - context.read().add( - CheckoutEvent.loadDraftOrder( - data!), - ); - context.push(PaymentTablePage( - table: widget.table, - draftOrder: data!, - )); - }, - label: 'Selesai'); - }, - ), - ), - ], - ), - ], - ), - actions: [ - TextButton( - child: - Text('Close', style: TextStyle(color: AppColors.primary)), - onPressed: () => Navigator.of(context).pop(), - ), - ], - ); - }, - ); - }, - child: Container( - padding: const EdgeInsets.all(16.0), - alignment: Alignment.center, - decoration: BoxDecoration( - color: widget.table.status == 'available' - ? AppColors.primary - : AppColors.red, - shape: BoxShape.rectangle, - borderRadius: BorderRadius.circular(10), - ), - child: Text('${widget.table.tableName}', - style: TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.w600, - )), - ), - ); - } + final int capacity = table.capacity ?? 0; + final chairDist = getChairDistribution(capacity); - Widget _buildInfoRow(String label, String value, {Color? color}) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: Row( - children: [ - Text( - label, - style: TextStyle(fontWeight: FontWeight.w600), + Widget chair() => Container( + width: 20, + height: 10, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(4), ), - SizedBox(width: 8), - Expanded( - child: Text( - value, - style: TextStyle( - color: color ?? Colors.black87, + ); + + return Container( + width: 120, + height: 80, + child: Stack( + alignment: Alignment.center, + children: [ + // Meja utama + Container( + width: 100, + height: 60, + decoration: BoxDecoration( + color: Colors.white, + border: Border.all( + color: getBorderColor(), + width: 2, + ), + borderRadius: BorderRadius.circular(16), + ), + child: Center( + child: CircleAvatar( + radius: 24, + backgroundColor: getStatusColor(), + child: Text( + table.tableName ?? "", + style: TextStyle( + color: getBorderColor(), + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), ), ), ), + // Kursi atas + if (chairDist['top']! > 0) + Positioned( + top: 0, + left: 10, + right: 10, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: List.generate(chairDist['top']!, (_) => chair()), + ), + ), + // Kursi bawah + if (chairDist['bottom']! > 0) + Positioned( + bottom: 0, + left: 10, + right: 10, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: List.generate(chairDist['bottom']!, (_) => chair()), + ), + ), + // Kursi kiri + if (chairDist['left']! > 0) + Positioned( + left: 0, + top: 15, + bottom: 15, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: List.generate(chairDist['left']!, (_) => chair()), + ), + ), + // Kursi kanan + if (chairDist['right']! > 0) + Positioned( + right: 0, + top: 15, + bottom: 15, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: List.generate(chairDist['right']!, (_) => chair()), + ), + ), + // Icon info kecil di pojok kanan atas jika status reserved + if (parseStatus(table.status) == TableStatus.occupied) + const Positioned( + top: 6, + right: 6, + child: + Icon(Icons.info_outline, size: 16, color: Colors.redAccent), + ), ], ), ); diff --git a/lib/presentation/table/widgets/table_widget.dart.backup b/lib/presentation/table/widgets/table_widget.dart.backup new file mode 100644 index 0000000..01f7917 --- /dev/null +++ b/lib/presentation/table/widgets/table_widget.dart.backup @@ -0,0 +1,399 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:enaklo_pos/core/components/buttons.dart'; +import 'package:enaklo_pos/core/components/custom_text_field.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/utils/date_formatter.dart'; +import 'package:enaklo_pos/data/datasources/product_local_datasource.dart'; + +import 'package:enaklo_pos/data/models/response/table_model.dart'; +import 'package:enaklo_pos/presentation/home/bloc/checkout/checkout_bloc.dart'; +import 'package:enaklo_pos/presentation/home/bloc/status_table/status_table_bloc.dart'; +import 'package:enaklo_pos/presentation/home/pages/dashboard_page.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/update_table/update_table_bloc.dart'; +import 'package:enaklo_pos/presentation/table/models/draft_order_model.dart'; + +import '../pages/payment_table_page.dart'; + +class TableWidget extends StatefulWidget { + final TableModel table; + const TableWidget({ + super.key, + required this.table, + }); + + @override + State createState() => _TableWidgetState(); +} + +class _TableWidgetState extends State { + TextEditingController? tableNameController; + DraftOrderModel? data; + @override + void initState() { + super.initState(); + loadData(); + tableNameController = TextEditingController(text: widget.table.tableName); + } + + @override + void dispose() { + tableNameController!.dispose(); + super.dispose(); + } + + loadData() async { + if (widget.table.status != 'available') { + // data = await ProductLocalDatasource.instance + // .getDraftOrderById(widget.table.orderId); + } + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () async { + if (widget.table.status == 'available') { + context.push(DashboardPage( + table: widget.table, + )); + } else { + // Handle occupied table click - load draft order and navigate to payment + context.read().add( + CheckoutEvent.loadDraftOrder(data!), + ); + log("Data Draft Order: ${data!.toMap()}"); + context.push(PaymentTablePage( + table: widget.table, + draftOrder: data!, + )); + } + }, + onLongPress: () { + // dialog info table + showDialog( + context: context, + builder: (context) { + return AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16)), + title: Row( + children: [ + Icon(Icons.table_bar, color: AppColors.primary), + SizedBox(width: 8), + Text('Table ${widget.table.tableName}'), + Spacer(), + BlocListener( + listener: (context, state) { + state.maybeWhen( + orElse: () {}, + success: (message) { + context + .read() + .add(const GetTableEvent.getTables()); + context.pop(); + }); + }, + child: IconButton( + onPressed: () { + // show dialaog adn input table name + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text('Update Table'), + content: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: 180, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CustomTextField( + controller: tableNameController!, + label: 'Table Name', + ), + SpaceHeight(16), + Row( + children: [ + Expanded( + child: Button.outlined( + onPressed: () { + context.pop(); + }, + label: 'close', + ), + ), + SpaceWidth(16), + Expanded( + child: Button.filled( + onPressed: () { + // final newData = + // TableModel( + // id: widget.table.id, + // tableName: + // tableNameController! + // .text, + // status: + // widget.table.status, + // startTime: widget + // .table.startTime, + // orderId: widget + // .table.orderId, + // paymentAmount: widget + // .table + // .paymentAmount, + // position: widget + // .table.position, + // ); + // context + // .read< + // UpdateTableBloc>() + // .add( + // UpdateTableEvent + // .updateTable( + // newData, + // ), + // ); + context + .pop(); // close dialog after adding + }, + label: 'Update', + ), + ) + ], + ) + ], + ), + ), + ), + actions: []); + }); + }, + icon: Icon(Icons.edit)), + ), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildInfoRow( + 'Status:', + widget.table.status == 'available' + ? 'Available' + : 'Occupied', + color: widget.table.status == 'available' + ? Colors.green + : Colors.red), + // widget.table.status == 'available' + // ? SizedBox.shrink() + // : _buildInfoRow( + // 'Start Time:', + // DateFormatter.formatDateTime2( + // widget.table.startTime)), + // widget.table.status == 'available' + // ? SizedBox.shrink() + // : _buildInfoRow( + // 'Order ID:', widget.table.orderId.toString()), + widget.table.status == 'available' + ? SizedBox.shrink() + : SpaceHeight(16), + widget.table.status == 'available' + ? SizedBox.shrink() + : Row( + children: [ + Expanded( + child: Button.outlined( + onPressed: () { + // Show void confirmation dialog + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Row( + children: [ + Icon(Icons.warning, + color: AppColors.red), + SizedBox(width: 8), + Text('Void Order?'), + ], + ), + content: Text( + 'Apakah anda yakin ingin membatalkan pesanan untuk meja ${widget.table.tableName}?\n\nPesanan akan dihapus secara permanen.'), + actions: [ + TextButton( + onPressed: () => + Navigator.pop(context), + child: Text('Tidak', + style: TextStyle( + color: AppColors.primary)), + ), + BlocListener( + listener: (context, state) { + state.maybeWhen( + orElse: () {}, + success: () { + context + .read() + .add(const GetTableEvent + .getTables()); + Navigator.pop( + context); // Close void dialog + Navigator.pop( + context); // Close table info dialog + ScaffoldMessenger.of(context) + .showSnackBar( + const SnackBar( + content: Text( + 'Pesanan berhasil dibatalkan'), + backgroundColor: + AppColors.primary, + ), + ); + }, + ); + }, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.red, + ), + onPressed: () { + // // Void the order + // final newTable = TableModel( + // id: widget.table.id, + // tableName: + // widget.table.tableName, + // status: 'available', + // orderId: 0, + // paymentAmount: 0, + // startTime: DateTime.now() + // .toIso8601String(), + // position: widget.table.position, + // ); + // context + // .read() + // .add( + // StatusTableEvent + // .statusTabel(newTable), + // ); + // // Remove draft order from local storage + // ProductLocalDatasource.instance + // .removeDraftOrderById( + // widget.table.orderId); + // log("Voided order for table: ${widget.table.tableName}"); + }, + child: const Text( + "Ya, Batalkan", + style: TextStyle( + color: Colors.white), + ), + ), + ), + ], + ), + ); + }, + label: 'Void Order', + color: AppColors.red, + textColor: AppColors.red, + ), + ), + SizedBox(width: 12), + Expanded( + child: BlocConsumer( + listener: (context, state) { + state.maybeWhen( + orElse: () {}, + success: () { + context.read().add( + const GetTableEvent.getTables()); + context.pop(); + }); + }, + builder: (context, state) { + return Button.filled( + onPressed: () { + context.pop(); + context.read().add( + CheckoutEvent.loadDraftOrder( + data!), + ); + context.push(PaymentTablePage( + table: widget.table, + draftOrder: data!, + )); + }, + label: 'Selesai'); + }, + ), + ), + ], + ), + ], + ), + actions: [ + TextButton( + child: + Text('Close', style: TextStyle(color: AppColors.primary)), + onPressed: () => Navigator.of(context).pop(), + ), + ], + ); + }, + ); + }, + child: Container( + padding: const EdgeInsets.all(16.0), + alignment: Alignment.center, + decoration: BoxDecoration( + color: widget.table.status == 'available' + ? AppColors.primary + : AppColors.red, + shape: BoxShape.rectangle, + borderRadius: BorderRadius.circular(10), + ), + child: Text('${widget.table.tableName}', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.w600, + )), + ), + ); + } + + Widget _buildInfoRow(String label, String value, {Color? color}) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: Row( + children: [ + Text( + label, + style: TextStyle(fontWeight: FontWeight.w600), + ), + SizedBox(width: 8), + Expanded( + child: Text( + value, + style: TextStyle( + color: color ?? Colors.black87, + ), + ), + ), + ], + ), + ); + } +} + +