2025-08-08 14:22:12 +07:00

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'),
],
),
),
],
);
}
}