This commit is contained in:
efrilm
2025-10-26 16:09:56 +07:00
parent 0e83d213fc
commit 655902a659
23 changed files with 3519 additions and 6 deletions
@@ -1,17 +1,238 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../../application/table/table_loader/table_loader_bloc.dart';
import '../../../../../common/extension/extension.dart';
import '../../../../../common/theme/theme.dart';
import '../../../../../domain/table/table.dart' as t;
import '../../../../../injection.dart';
import '../../../../components/loader/loader_with_text.dart';
import 'widgets/table_card.dart';
@RoutePage()
class TablePage extends StatelessWidget {
class TablePage extends StatefulWidget implements AutoRouteWrapper {
const TablePage({super.key});
@override
State<TablePage> createState() => _TablePageState();
@override
Widget wrappedRoute(BuildContext context) => BlocProvider(
create: (context) =>
getIt<TableLoaderBloc>()
..add(TableLoaderEvent.fetched(isRefresh: true)),
child: this,
);
}
class _TablePageState extends State<TablePage> {
t.Table? selectedTable;
t.Table? draggingTable;
Offset? tapPosition;
@override
Widget build(BuildContext context) {
return Center(
child: Text(
'Table Page',
style: Theme.of(context).textTheme.headlineMedium,
final double mapWidth = context.deviceWidth * 2;
final double mapHeight = context.deviceHeight * 1.5;
return Scaffold(
backgroundColor: AppColor.background,
appBar: AppBar(
title: const Text("Layout Meja"),
backgroundColor: Colors.white,
foregroundColor: Colors.black,
elevation: 0.5,
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () {
context.read<TableLoaderBloc>().add(
const TableLoaderEvent.fetched(isRefresh: true),
);
},
),
IconButton(
icon: const Icon(Icons.add),
onPressed: () {
// showDialog(
// context: context,
// builder: (context) => FormTableNewDialog(),
// );
},
),
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(20),
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"),
],
),
),
),
),
body: BlocBuilder<TableLoaderBloc, TableLoaderState>(
builder: (context, state) {
if (state.isFetching) {
return const Center(child: LoaderWithText());
}
return SafeArea(
child: Stack(
children: [
Row(
children: [
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: [
...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],
),
),
),
...state.tables.map((table) {
final isSelected = selectedTable == table;
return Positioned(
left: table.positionX,
top: table.positionY,
child: Draggable<t.Table>(
data: table,
feedback: Material(
color: Colors.transparent,
child: TableCard(
table: table,
isSelected: isSelected,
),
),
childWhenDragging: Opacity(
opacity: 0.5,
child: TableCard(
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 = table.copyWith(
positionX: local.dx.clamp(
0,
mapWidth - 120,
),
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: () {},
onTapDown: (details) =>
tapPosition = details.globalPosition,
onLongPress: () {
if (table.status.isOccupied) {
// _showPopupMenu(context, table);
}
},
child: TableCard(
table: table,
isSelected: isSelected,
),
),
),
);
}),
],
),
),
),
),
],
),
],
),
);
},
),
);
}
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)),
],
);
}
}
@@ -0,0 +1,146 @@
import 'package:flutter/material.dart';
import '../../../../../../common/function/app_function.dart';
import '../../../../../../common/theme/theme.dart';
import '../../../../../../domain/table/table.dart' as t;
import '../../../../../../domain/table/table.dart';
class TableCard extends StatelessWidget {
final t.Table table;
final bool isSelected;
const TableCard({super.key, required this.table, required this.isSelected});
Color getStatusColor() {
switch (table.status) {
case TableStatusType.available:
return Colors.blue[100]!;
case TableStatusType.occupied:
return Colors.orange[100]!;
default:
return Colors.grey[200]!;
}
}
Color getBorderColor() {
if (isSelected) return AppColor.primary;
switch (table.status) {
case TableStatusType.available:
return Colors.blue;
case TableStatusType.occupied:
return Colors.orange;
default:
return Colors.grey;
}
}
@override
Widget build(BuildContext context) {
final int capacity = table.capacity;
final chairDist = getChairDistribution(capacity);
Widget chair() => Container(
width: 20,
height: 10,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(4),
),
);
return SizedBox(
width: table.capacity > 16
? 240
: table.capacity > 8
? 180
: 120,
height: 80,
child: Stack(
alignment: Alignment.center,
children: [
Container(
width: table.capacity > 16
? 220
: table.capacity > 8
? 160
: 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,
),
),
),
),
),
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 (table.status.isOccupied)
const Positioned(
top: 6,
right: 6,
child: Icon(
Icons.info_outline,
size: 16,
color: Colors.redAccent,
),
),
],
),
);
}
}
+1 -1
View File
@@ -168,7 +168,7 @@ class TableRoute extends _i10.PageRouteInfo<void> {
static _i10.PageInfo page = _i10.PageInfo(
name,
builder: (data) {
return const _i9.TablePage();
return _i10.WrappedRoute(child: const _i9.TablePage());
},
);
}