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