458 lines
18 KiB
Dart
458 lines
18 KiB
Dart
import 'package:enaklo_pos/core/components/flushbar.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/blocs/transfer_table/transfer_table_bloc.dart';
|
|
import 'package:enaklo_pos/presentation/table/dialogs/form_table_new_dialog.dart';
|
|
import 'package:enaklo_pos/presentation/table/dialogs/transfer_table_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});
|
|
|
|
@override
|
|
State<TablePage> createState() => _TablePageState();
|
|
}
|
|
|
|
class _TablePageState extends State<TablePage> {
|
|
TableModel? selectedTable;
|
|
|
|
// Untuk drag
|
|
TableModel? draggingTable;
|
|
|
|
Offset? _tapPosition;
|
|
|
|
// Ubah function toggleSelectTable menjadi selectTable
|
|
void selectTable(TableModel table) {
|
|
if (table.status == 'occupied') return;
|
|
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());
|
|
super.initState();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
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: BlocListener<TransferTableBloc, TransferTableState>(
|
|
listener: (context, state) {
|
|
state.maybeWhen(
|
|
orElse: () {},
|
|
success: () {
|
|
AppFlushbar.showSuccess(context, 'Transfer Table Success');
|
|
context.read<GetTableBloc>().add(const GetTableEvent.getTables());
|
|
},
|
|
error: (message) {
|
|
AppFlushbar.showError(context, message);
|
|
},
|
|
);
|
|
},
|
|
child: BlocBuilder<GetTableBloc, GetTableState>(
|
|
builder: (context, state) {
|
|
return state.maybeWhen(
|
|
orElse: () => SizedBox.shrink(),
|
|
loading: () => const Center(child: CircularProgressIndicator()),
|
|
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: Offset(
|
|
table.positionX ?? 0.0,
|
|
table.positionY ?? 0.0,
|
|
),
|
|
));
|
|
},
|
|
child: GestureDetector(
|
|
onTap: () => selectTable(table),
|
|
onTapDown: (details) => _tapPosition =
|
|
details.globalPosition,
|
|
onLongPress: () {
|
|
if (table.status == 'occupied') {
|
|
_showPopupMenu(context, 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 {}
|
|
},
|
|
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)),
|
|
],
|
|
);
|
|
}
|
|
|
|
void _showPopupMenu(BuildContext context, TableModel table) {
|
|
final RenderBox overlay =
|
|
Overlay.of(context).context.findRenderObject() as RenderBox;
|
|
|
|
showMenu(
|
|
context: context,
|
|
position: RelativeRect.fromRect(
|
|
_tapPosition != null
|
|
? Rect.fromLTWH(_tapPosition!.dx, _tapPosition!.dy, 0, 0)
|
|
: Rect.fromLTWH(100, 100, 0, 0),
|
|
Offset.zero & overlay.size,
|
|
),
|
|
color: AppColors.white,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
|
elevation: 1,
|
|
items: [
|
|
PopupMenuItem(
|
|
onTap: () {
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => TransferTableDialog(fromTable: table),
|
|
);
|
|
},
|
|
child: Row(
|
|
children: [
|
|
Icon(Icons.swap_horiz),
|
|
SizedBox(width: 8),
|
|
Text('Transfer'),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|