dev #1

Merged
aefril merged 128 commits from dev into main 2025-08-13 17:19:48 +00:00
6 changed files with 1021 additions and 455 deletions
Showing only changes of commit 5af0259f0a - Show all commits

View File

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

View File

@ -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<DashboardPage> {
isTable: false,
table: widget.table,
),
// const TablePage(),
TableManagementScreen(),
const TablePage(),
const ReportPage(),
const PrinterConfigurationPage(),
// SalesPage(),

View File

@ -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<TablePage> {
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<GetTableBloc>().add(const GetTableEvent.getTables());
@ -22,74 +61,340 @@ class _TablePageState extends State<TablePage> {
@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<GetTableBloc, GetTableState>(
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<CreateTableBloc, CreateTableState>(
listener: (context, state) {
state.maybeWhen(
orElse: () {},
success: (message) {
context
.read<GetTableBloc>()
.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<GetTableBloc, GetTableState>(
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<TableModel>(
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<ChangePositionTableBloc>()
.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<CheckoutBloc>().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)),
],
);
}
}

View File

@ -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<TablePage> createState() => _TablePageState();
}
class _TablePageState extends State<TablePage> {
@override
void initState() {
context.read<GetTableBloc>().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<GetTableBloc, GetTableState>(
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],
);
},
);
},
);
},
),
],
),
);
}
}

View File

@ -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<TableWidget> createState() => _TableWidgetState();
}
const TableWidget({super.key, required this.table, this.isSelected = false});
class _TableWidgetState extends State<TableWidget> {
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<String, int> 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<CheckoutBloc>().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<UpdateTableBloc, UpdateTableState>(
listener: (context, state) {
state.maybeWhen(
orElse: () {},
success: (message) {
context
.read<GetTableBloc>()
.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<StatusTableBloc,
StatusTableState>(
listener: (context, state) {
state.maybeWhen(
orElse: () {},
success: () {
context
.read<GetTableBloc>()
.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<StatusTableBloc>()
// .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<StatusTableBloc,
StatusTableState>(
listener: (context, state) {
state.maybeWhen(
orElse: () {},
success: () {
context.read<GetTableBloc>().add(
const GetTableEvent.getTables());
context.pop();
});
},
builder: (context, state) {
return Button.filled(
onPressed: () {
context.pop();
context.read<CheckoutBloc>().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),
),
],
),
);

View File

@ -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<TableWidget> createState() => _TableWidgetState();
}
class _TableWidgetState extends State<TableWidget> {
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<CheckoutBloc>().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<UpdateTableBloc, UpdateTableState>(
listener: (context, state) {
state.maybeWhen(
orElse: () {},
success: (message) {
context
.read<GetTableBloc>()
.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<StatusTableBloc,
StatusTableState>(
listener: (context, state) {
state.maybeWhen(
orElse: () {},
success: () {
context
.read<GetTableBloc>()
.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<StatusTableBloc>()
// .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<StatusTableBloc,
StatusTableState>(
listener: (context, state) {
state.maybeWhen(
orElse: () {},
success: () {
context.read<GetTableBloc>().add(
const GetTableEvent.getTables());
context.pop();
});
},
builder: (context, state) {
return Button.filled(
onPressed: () {
context.pop();
context.read<CheckoutBloc>().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,
),
),
),
],
),
);
}
}