chore: Flutter mobile app, CI, and dev tooling

- mobile/: Flutter/Dart merchant mobile app skeleton
- .github/: GitHub Actions CI workflows
- .dockerignore: exclude host node_modules from build context
- .cursorrules: Cursor IDE project rules
- .claude/: Claude Code project settings and launch config

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-05-27 21:35:27 +03:30
parent 42d4cb896a
commit a85890f30a
52 changed files with 3919 additions and 0 deletions
@@ -0,0 +1,109 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import '../cart/cart_state.dart';
import '../table/table_context.dart';
class QrScanScreen extends ConsumerStatefulWidget {
const QrScanScreen({super.key});
@override
ConsumerState<QrScanScreen> createState() => _QrScanScreenState();
}
class _QrScanScreenState extends ConsumerState<QrScanScreen> {
final _manualController = TextEditingController(text: 'demo_table_01');
bool _loading = false;
bool _handled = false;
@override
void dispose() {
_manualController.dispose();
super.dispose();
}
Future<void> _resolve(String raw) async {
final code = parseQrCode(raw);
if (code == null || code.isEmpty) return;
setState(() => _loading = true);
try {
final data = await ref.read(publicApiProvider).resolveQr(code);
if (!mounted || data == null) return;
final slug = data['cafeSlug'] as String;
final tableId = data['tableId'] as String?;
final tableNumber = data['tableNumber']?.toString();
ref.read(tableContextProvider.notifier).setTable(
tableId: tableId ?? '',
tableNumber: tableNumber ?? '',
cafeSlug: slug,
);
if (tableId != null) {
ref.read(cartProvider.notifier).setTable(tableId);
}
context.push(
'/cafe/$slug/menu?tableId=$tableId&tableNumber=$tableNumber',
);
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('خطا: $e')),
);
}
} finally {
if (mounted) setState(() => _loading = false);
}
}
void _onDetect(BarcodeCapture capture) {
if (_handled || _loading) return;
final raw = capture.barcodes.firstOrNull?.rawValue;
if (raw == null) return;
_handled = true;
_resolve(raw);
}
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.rtl,
child: Scaffold(
appBar: AppBar(title: const Text('اسکن QR میز')),
body: Column(
children: [
Expanded(
child: MobileScanner(onDetect: _onDetect),
),
Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
TextField(
controller: _manualController,
decoration: const InputDecoration(
labelText: 'کد QR (دستی)',
hintText: 'demo_table_01',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
FilledButton(
onPressed: _loading ? null : () => _resolve(_manualController.text),
child: _loading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('باز کردن منو'),
),
],
),
),
],
),
),
);
}
}