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:
@@ -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('باز کردن منو'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user