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,127 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shamsi_date/shamsi_date.dart';
import '../cart/cart_state.dart';
class ReserveScreen extends ConsumerStatefulWidget {
const ReserveScreen({super.key, required this.slug});
final String slug;
@override
ConsumerState<ReserveScreen> createState() => _ReserveScreenState();
}
class _ReserveScreenState extends ConsumerState<ReserveScreen> {
final _nameController = TextEditingController();
final _phoneController = TextEditingController();
final _notesController = TextEditingController();
Jalali? _date;
TimeOfDay _time = const TimeOfDay(hour: 19, minute: 0);
int _partySize = 2;
bool _submitting = false;
String? _message;
@override
void dispose() {
_nameController.dispose();
_phoneController.dispose();
_notesController.dispose();
super.dispose();
}
String _formatDate(Jalali d) =>
'${d.year.toString().padLeft(4, '0')}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}';
Future<void> _submit() async {
if (_date == null) {
setState(() => _message = 'تاریخ را انتخاب کنید');
return;
}
setState(() {
_submitting = true;
_message = null;
});
try {
await ref.read(publicApiProvider).createReservation(
widget.slug,
guestName: _nameController.text,
guestPhone: _phoneController.text,
date: _formatDate(_date!),
time: '${_time.hour.toString().padLeft(2, '0')}:${_time.minute.toString().padLeft(2, '0')}:00',
partySize: _partySize,
notes: _notesController.text.isEmpty ? null : _notesController.text,
);
setState(() => _message = 'رزرو ثبت شد — منتظر تأیید کافه باشید');
} catch (_) {
setState(() => _message = 'خطا در ثبت رزرو');
} finally {
setState(() => _submitting = false);
}
}
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.rtl,
child: Scaffold(
appBar: AppBar(title: const Text('رزرو میز')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
TextField(controller: _nameController, decoration: const InputDecoration(labelText: 'نام')),
TextField(
controller: _phoneController,
decoration: const InputDecoration(labelText: 'موبایل'),
keyboardType: TextInputType.phone,
),
ListTile(
title: Text(_date == null ? 'تاریخ' : _formatDate(_date!)),
trailing: const Icon(Icons.calendar_today),
onTap: () async {
final picked = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime.now(),
lastDate: DateTime.now().add(const Duration(days: 90)),
);
if (picked != null) setState(() => _date = Jalali.fromDateTime(picked));
},
),
ListTile(
title: Text('ساعت ${_time.format(context)}'),
trailing: const Icon(Icons.access_time),
onTap: () async {
final picked = await showTimePicker(context: context, initialTime: _time);
if (picked != null) setState(() => _time = picked);
},
),
Row(
children: [
const Text('تعداد نفر:'),
IconButton(onPressed: () => setState(() => _partySize = (_partySize - 1).clamp(1, 20)), icon: const Icon(Icons.remove)),
Text('$_partySize'),
IconButton(onPressed: () => setState(() => _partySize = (_partySize + 1).clamp(1, 20)), icon: const Icon(Icons.add)),
],
),
TextField(
controller: _notesController,
decoration: const InputDecoration(labelText: 'یادداشت'),
maxLines: 2,
),
const SizedBox(height: 16),
FilledButton(
onPressed: _submitting ? null : _submit,
child: _submitting ? const CircularProgressIndicator() : const Text('ثبت رزرو'),
),
if (_message != null) ...[
const SizedBox(height: 12),
Text(_message!, style: TextStyle(color: Theme.of(context).colorScheme.primary)),
],
],
),
),
);
}
}