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,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)),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user