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,31 @@
import 'package:dio/dio.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'api_config.dart';
class ApiClient {
ApiClient({Dio? dio, FlutterSecureStorage? storage})
: _storage = storage ?? const FlutterSecureStorage(),
_dio = dio ??
Dio(BaseOptions(
baseUrl: apiBaseUrl,
connectTimeout: const Duration(seconds: 15),
receiveTimeout: const Duration(seconds: 15),
headers: {'Content-Type': 'application/json'},
)) {
_dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) async {
final token = await _storage.read(key: 'access_token');
if (token != null && token.isNotEmpty) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
},
));
}
final Dio _dio;
final FlutterSecureStorage _storage;
Dio get dio => _dio;
}
@@ -0,0 +1,4 @@
const String apiBaseUrl = String.fromEnvironment(
'MEEZI_API_URL',
defaultValue: 'http://localhost:5080',
);
@@ -0,0 +1,36 @@
enum MenuItemVisualKind { food, drink }
const _drinkCategoryIds = {'cat_demo_drinks', 'cat_demo_cold'};
const _drinkHints = [
'drink',
'cold',
'coffee',
'tea',
'juice',
'smoothie',
'beverage',
'bar',
'نوشیدنی',
'سرد',
'گرم',
'قهوه',
'چای',
'آبمیوه',
'اسموتی',
'مشروب',
'بار',
];
MenuItemVisualKind inferMenuItemKind({
required String categoryId,
String? categoryName,
}) {
if (_drinkCategoryIds.contains(categoryId)) return MenuItemVisualKind.drink;
final haystack = '$categoryId ${categoryName ?? ''}'.toLowerCase();
for (final hint in _drinkHints) {
if (haystack.contains(hint)) return MenuItemVisualKind.drink;
}
return MenuItemVisualKind.food;
}
@@ -0,0 +1,22 @@
/// Queues HR attendance actions when offline; syncs on reconnect.
class SyncEngine {
final List<Map<String, dynamic>> _queue = [];
List<Map<String, dynamic>> get pending => List.unmodifiable(_queue);
void enqueueAttendance({
required String action,
required String cafeId,
required String employeeId,
}) {
_queue.add({
'type': 'attendance',
'action': action,
'cafeId': cafeId,
'employeeId': employeeId,
'at': DateTime.now().toUtc().toIso8601String(),
});
}
void clear() => _queue.clear();
}
@@ -0,0 +1 @@
String formatToman(num value) => '${value.toStringAsFixed(0)} ت';