Add pricing entry flow and platform admin foundations
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
import '../api/pocketbase_client.dart';
|
||||
import '../../models/platform_admin.dart';
|
||||
import '../../models/tenant.dart';
|
||||
import '../../models/user_profile.dart';
|
||||
|
||||
@@ -73,10 +74,24 @@ class AuthRepository {
|
||||
String id, {
|
||||
String? companyName,
|
||||
String? defaultCurrency,
|
||||
String? companyAddress,
|
||||
String? city,
|
||||
String? district,
|
||||
double? latitude,
|
||||
double? longitude,
|
||||
List<String>? workflowOverrides,
|
||||
}) async {
|
||||
final body = <String, dynamic>{};
|
||||
if (companyName != null) body['company_name'] = companyName;
|
||||
if (defaultCurrency != null) body['default_currency'] = defaultCurrency;
|
||||
if (companyAddress != null) body['company_address'] = companyAddress;
|
||||
if (city != null) body['city'] = city;
|
||||
if (district != null) body['district'] = district;
|
||||
if (latitude != null) body['latitude'] = latitude;
|
||||
if (longitude != null) body['longitude'] = longitude;
|
||||
if (workflowOverrides != null) {
|
||||
body['workflow_overrides'] = workflowOverrides;
|
||||
}
|
||||
if (body.isEmpty) return;
|
||||
await _pb.collection('tenants').update(id, body: body);
|
||||
}
|
||||
@@ -85,26 +100,51 @@ class AuthRepository {
|
||||
final record = _pb.authStore.record!;
|
||||
final user = UserProfile.fromJson(record.toJson());
|
||||
List<TenantMembership> tenants = [];
|
||||
List<PlatformMembership> platformMemberships = [];
|
||||
try {
|
||||
tenants = await _fetchUserTenants(record.id);
|
||||
} catch (_) {}
|
||||
return AuthResult(user: user, tenants: tenants);
|
||||
try {
|
||||
platformMemberships = await _fetchPlatformMemberships(record.id);
|
||||
} catch (_) {}
|
||||
return AuthResult(
|
||||
user: user,
|
||||
tenants: tenants,
|
||||
platformMemberships: platformMemberships,
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<TenantMembership>> _fetchUserTenants(String userId) async {
|
||||
final result = await _pb.collection('tenant_members').getList(
|
||||
filter: 'user_id = "$userId"',
|
||||
expand: 'tenant_id',
|
||||
perPage: 50,
|
||||
);
|
||||
filter: 'user_id = "$userId"',
|
||||
expand: 'tenant_id',
|
||||
perPage: 50,
|
||||
);
|
||||
return result.items
|
||||
.map((r) => TenantMembership.fromJson(r.toJson()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<List<PlatformMembership>> _fetchPlatformMemberships(
|
||||
String userId,
|
||||
) async {
|
||||
final result = await _pb.collection('platform_memberships').getList(
|
||||
filter: 'user_id = "$userId"',
|
||||
perPage: 20,
|
||||
);
|
||||
return result.items
|
||||
.map((r) => PlatformMembership.fromJson(r.toJson()))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
class AuthResult {
|
||||
const AuthResult({required this.user, required this.tenants});
|
||||
const AuthResult({
|
||||
required this.user,
|
||||
required this.tenants,
|
||||
this.platformMemberships = const [],
|
||||
});
|
||||
final UserProfile user;
|
||||
final List<TenantMembership> tenants;
|
||||
final List<PlatformMembership> platformMemberships;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
|
||||
class LocationAccessService {
|
||||
LocationAccessService._();
|
||||
|
||||
static Future<Position> getCurrentPosition() async {
|
||||
final enabled = await Geolocator.isLocationServiceEnabled();
|
||||
if (!enabled) {
|
||||
throw Exception(
|
||||
'Konum servisleri kapalı. Lütfen cihaz ayarlarından açın.');
|
||||
}
|
||||
|
||||
var permission = await Geolocator.checkPermission();
|
||||
if (permission == LocationPermission.denied) {
|
||||
permission = await Geolocator.requestPermission();
|
||||
}
|
||||
|
||||
if (permission == LocationPermission.denied) {
|
||||
throw Exception('Konum izni verilmedi.');
|
||||
}
|
||||
|
||||
if (permission == LocationPermission.deniedForever) {
|
||||
throw Exception(
|
||||
'Konum izni kalıcı olarak reddedildi. Lütfen cihaz ayarlarından izin verin.',
|
||||
);
|
||||
}
|
||||
|
||||
return Geolocator.getCurrentPosition();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
class OpenFreeMap {
|
||||
OpenFreeMap._();
|
||||
|
||||
static const libertyStyle = 'https://tiles.openfreemap.org/styles/liberty';
|
||||
static const positronStyle = 'https://tiles.openfreemap.org/styles/positron';
|
||||
static const brightStyle = 'https://tiles.openfreemap.org/styles/bright';
|
||||
static const darkStyle = 'https://tiles.openfreemap.org/styles/dark';
|
||||
static const fiordStyle = 'https://tiles.openfreemap.org/styles/fiord';
|
||||
|
||||
static const attribution =
|
||||
'OpenFreeMap © OpenMapTiles Data from OpenStreetMap';
|
||||
}
|
||||
@@ -3,8 +3,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
import '../auth/auth_repository.dart';
|
||||
import '../services/notification_service.dart';
|
||||
import '../../models/platform_admin.dart';
|
||||
import '../../models/tenant.dart';
|
||||
import '../../models/user_profile.dart';
|
||||
import '../../features/shared/tenant_location_data.dart';
|
||||
import 'locale_provider.dart';
|
||||
|
||||
class AuthState {
|
||||
@@ -12,6 +14,7 @@ class AuthState {
|
||||
this.profile,
|
||||
this.activeTenant,
|
||||
this.memberships = const [],
|
||||
this.platformMemberships = const [],
|
||||
this.isLoading = true,
|
||||
this.error,
|
||||
});
|
||||
@@ -19,15 +22,24 @@ class AuthState {
|
||||
final UserProfile? profile;
|
||||
final TenantMembership? activeTenant;
|
||||
final List<TenantMembership> memberships;
|
||||
final List<PlatformMembership> platformMemberships;
|
||||
final bool isLoading;
|
||||
final String? error;
|
||||
|
||||
bool get isAuthenticated => profile != null;
|
||||
bool get isSuperAdmin => platformMemberships.any((m) => m.isSuperAdmin);
|
||||
PlatformMembership? get primaryPlatformMembership {
|
||||
for (final membership in platformMemberships) {
|
||||
if (membership.isActive) return membership;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
AuthState copyWith({
|
||||
UserProfile? profile,
|
||||
TenantMembership? activeTenant,
|
||||
List<TenantMembership>? memberships,
|
||||
List<PlatformMembership>? platformMemberships,
|
||||
bool? isLoading,
|
||||
String? error,
|
||||
bool clearError = false,
|
||||
@@ -36,6 +48,7 @@ class AuthState {
|
||||
profile: profile ?? this.profile,
|
||||
activeTenant: activeTenant ?? this.activeTenant,
|
||||
memberships: memberships ?? this.memberships,
|
||||
platformMemberships: platformMemberships ?? this.platformMemberships,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
error: clearError ? null : (error ?? this.error),
|
||||
);
|
||||
@@ -60,11 +73,12 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
||||
state = AuthState(
|
||||
profile: result.user,
|
||||
memberships: result.tenants,
|
||||
activeTenant:
|
||||
result.tenants.isEmpty ? null : result.tenants.first,
|
||||
platformMemberships: result.platformMemberships,
|
||||
activeTenant: result.tenants.isEmpty ? null : result.tenants.first,
|
||||
isLoading: false,
|
||||
);
|
||||
final isLab = result.tenants.isNotEmpty && result.tenants.first.tenant.isLab;
|
||||
final isLab =
|
||||
result.tenants.isNotEmpty && result.tenants.first.tenant.isLab;
|
||||
NotificationService.loginUser(result.user.id, isLab: isLab);
|
||||
_applyLocale(result.user.preferredLanguage);
|
||||
} catch (_) {
|
||||
@@ -93,11 +107,12 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
||||
state = AuthState(
|
||||
profile: result.user,
|
||||
memberships: result.tenants,
|
||||
activeTenant:
|
||||
result.tenants.isEmpty ? null : result.tenants.first,
|
||||
platformMemberships: result.platformMemberships,
|
||||
activeTenant: result.tenants.isEmpty ? null : result.tenants.first,
|
||||
isLoading: false,
|
||||
);
|
||||
final isLab = result.tenants.isNotEmpty && result.tenants.first.tenant.isLab;
|
||||
final isLab =
|
||||
result.tenants.isNotEmpty && result.tenants.first.tenant.isLab;
|
||||
NotificationService.loginUser(result.user.id, isLab: isLab);
|
||||
_applyLocale(result.user.preferredLanguage);
|
||||
} catch (e) {
|
||||
@@ -122,8 +137,8 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
||||
state = AuthState(
|
||||
profile: result.user,
|
||||
memberships: result.tenants,
|
||||
activeTenant:
|
||||
result.tenants.isEmpty ? null : result.tenants.first,
|
||||
platformMemberships: result.platformMemberships,
|
||||
activeTenant: result.tenants.isEmpty ? null : result.tenants.first,
|
||||
isLoading: false,
|
||||
);
|
||||
} catch (e) {
|
||||
@@ -157,6 +172,7 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
||||
state = state.copyWith(
|
||||
profile: result.user,
|
||||
memberships: result.tenants,
|
||||
platformMemberships: result.platformMemberships,
|
||||
activeTenant: newActive,
|
||||
);
|
||||
} catch (_) {}
|
||||
@@ -172,11 +188,19 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
||||
required String tenantId,
|
||||
required String companyName,
|
||||
String? defaultCurrency,
|
||||
TenantLocationData? location,
|
||||
List<String>? workflowOverrides,
|
||||
}) async {
|
||||
await _repo.updateTenant(
|
||||
tenantId,
|
||||
companyName: companyName,
|
||||
defaultCurrency: defaultCurrency,
|
||||
companyAddress: location?.address,
|
||||
city: location?.city,
|
||||
district: location?.district,
|
||||
latitude: location?.latitude,
|
||||
longitude: location?.longitude,
|
||||
workflowOverrides: workflowOverrides,
|
||||
);
|
||||
await refresh();
|
||||
}
|
||||
@@ -194,8 +218,7 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
||||
}
|
||||
}
|
||||
|
||||
final authProvider =
|
||||
StateNotifierProvider<AuthNotifier, AuthState>((ref) {
|
||||
final authProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) {
|
||||
return AuthNotifier(
|
||||
onLocaleLoaded: (code) =>
|
||||
ref.read(localeProvider.notifier).setLocale(Locale(code)),
|
||||
|
||||
+282
-75
@@ -10,6 +10,7 @@ import '../../models/tenant.dart';
|
||||
import '../../features/auth/sign_in_screen.dart';
|
||||
import '../../features/auth/sign_up_screen.dart';
|
||||
import '../../features/auth/onboarding_screen.dart';
|
||||
import '../../features/auth/welcome_pricing_screen.dart';
|
||||
import '../../features/clinic/dashboard/clinic_dashboard_screen.dart';
|
||||
import '../../features/clinic/jobs/clinic_jobs_screen.dart';
|
||||
import '../../features/clinic/jobs/clinic_job_detail_screen.dart';
|
||||
@@ -37,6 +38,7 @@ import '../../models/connection.dart';
|
||||
const routeSignIn = '/sign-in';
|
||||
const routeSignUp = '/sign-up';
|
||||
const routeOnboarding = '/onboarding';
|
||||
const routeWelcome = '/welcome';
|
||||
|
||||
// Clinic routes
|
||||
const routeClinicDashboard = '/clinic/dashboard';
|
||||
@@ -65,15 +67,20 @@ const routeLabAi = '/lab/ai';
|
||||
const routeLabDiscounts = '/lab/discounts';
|
||||
|
||||
List<RouteBase> buildRoutes() => [
|
||||
GoRoute(
|
||||
path: routeWelcome, builder: (_, __) => const WelcomePricingScreen()),
|
||||
GoRoute(path: routeSignIn, builder: (_, __) => const SignInScreen()),
|
||||
GoRoute(path: routeSignUp, builder: (_, __) => const SignUpScreen()),
|
||||
GoRoute(path: routeOnboarding, builder: (_, __) => const OnboardingScreen()),
|
||||
GoRoute(
|
||||
path: routeOnboarding, builder: (_, __) => const OnboardingScreen()),
|
||||
|
||||
// ── Clinic shell ──────────────────────────────────────────────────────
|
||||
ShellRoute(
|
||||
builder: (context, state, child) => _ClinicShell(child: child),
|
||||
routes: [
|
||||
GoRoute(path: routeClinicDashboard, builder: (_, __) => const ClinicDashboardScreen()),
|
||||
GoRoute(
|
||||
path: routeClinicDashboard,
|
||||
builder: (_, __) => const ClinicDashboardScreen()),
|
||||
GoRoute(
|
||||
path: routeClinicJobs,
|
||||
builder: (_, __) => const ClinicJobsScreen(),
|
||||
@@ -81,7 +88,8 @@ List<RouteBase> buildRoutes() => [
|
||||
GoRoute(path: 'new', builder: (_, __) => const NewJobScreen()),
|
||||
GoRoute(
|
||||
path: ':jobId',
|
||||
builder: (_, s) => ClinicJobDetailScreen(jobId: s.pathParameters['jobId']!),
|
||||
builder: (_, s) =>
|
||||
ClinicJobDetailScreen(jobId: s.pathParameters['jobId']!),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -91,15 +99,25 @@ List<RouteBase> buildRoutes() => [
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: ':patientId',
|
||||
builder: (_, s) => ClinicPatientDetailScreen(patientId: s.pathParameters['patientId']!),
|
||||
builder: (_, s) => ClinicPatientDetailScreen(
|
||||
patientId: s.pathParameters['patientId']!),
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(path: routeClinicConnections, builder: (_, __) => const ClinicConnectionsScreen()),
|
||||
GoRoute(path: routeClinicFinance, builder: (_, __) => const ClinicFinanceScreen()),
|
||||
GoRoute(path: routeClinicSettings, builder: (_, __) => const ClinicSettingsScreen()),
|
||||
GoRoute(path: routeClinicReports, builder: (_, __) => const ReportsScreen()),
|
||||
GoRoute(path: routeClinicAi, builder: (_, __) => const AiChatScreen()),
|
||||
GoRoute(
|
||||
path: routeClinicConnections,
|
||||
builder: (_, __) => const ClinicConnectionsScreen()),
|
||||
GoRoute(
|
||||
path: routeClinicFinance,
|
||||
builder: (_, __) => const ClinicFinanceScreen()),
|
||||
GoRoute(
|
||||
path: routeClinicSettings,
|
||||
builder: (_, __) => const ClinicSettingsScreen()),
|
||||
GoRoute(
|
||||
path: routeClinicReports,
|
||||
builder: (_, __) => const ReportsScreen()),
|
||||
GoRoute(
|
||||
path: routeClinicAi, builder: (_, __) => const AiChatScreen()),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -107,19 +125,26 @@ List<RouteBase> buildRoutes() => [
|
||||
ShellRoute(
|
||||
builder: (context, state, child) => _LabShell(child: child),
|
||||
routes: [
|
||||
GoRoute(path: routeLabDashboard, builder: (_, __) => const LabDashboardScreen()),
|
||||
GoRoute(path: routeLabJobsInbound, builder: (_, __) => const LabJobsInboundScreen()),
|
||||
GoRoute(
|
||||
path: routeLabDashboard,
|
||||
builder: (_, __) => const LabDashboardScreen()),
|
||||
GoRoute(
|
||||
path: routeLabJobsInbound,
|
||||
builder: (_, __) => const LabJobsInboundScreen()),
|
||||
GoRoute(
|
||||
path: routeLabJobsAll,
|
||||
builder: (_, __) => const LabAllJobsScreen(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: ':jobId',
|
||||
builder: (_, s) => LabJobDetailScreen(jobId: s.pathParameters['jobId']!),
|
||||
builder: (_, s) =>
|
||||
LabJobDetailScreen(jobId: s.pathParameters['jobId']!),
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(path: routeLabProducts, builder: (_, __) => const LabProductsScreen()),
|
||||
GoRoute(
|
||||
path: routeLabProducts,
|
||||
builder: (_, __) => const LabProductsScreen()),
|
||||
GoRoute(
|
||||
path: routeLabConnections,
|
||||
builder: (_, __) => const LabConnectionsScreen(),
|
||||
@@ -141,10 +166,17 @@ List<RouteBase> buildRoutes() => [
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(path: routeLabDiscounts, builder: (_, __) => const DiscountsScreen()),
|
||||
GoRoute(path: routeLabFinance, builder: (_, __) => const LabFinanceScreen()),
|
||||
GoRoute(path: routeLabSettings, builder: (_, __) => const LabSettingsScreen()),
|
||||
GoRoute(path: routeLabReports, builder: (_, __) => const ReportsScreen()),
|
||||
GoRoute(
|
||||
path: routeLabDiscounts,
|
||||
builder: (_, __) => const DiscountsScreen()),
|
||||
GoRoute(
|
||||
path: routeLabFinance,
|
||||
builder: (_, __) => const LabFinanceScreen()),
|
||||
GoRoute(
|
||||
path: routeLabSettings,
|
||||
builder: (_, __) => const LabSettingsScreen()),
|
||||
GoRoute(
|
||||
path: routeLabReports, builder: (_, __) => const ReportsScreen()),
|
||||
GoRoute(path: routeLabAi, builder: (_, __) => const AiChatScreen()),
|
||||
],
|
||||
),
|
||||
@@ -216,11 +248,36 @@ class _ClinicShellState extends ConsumerState<_ClinicShell> {
|
||||
String _selectedRoute = routeClinicDashboard;
|
||||
|
||||
List<_NavItem> _clinicTopSingles(AppStrings s) => [
|
||||
_NavItem(route: routeClinicDashboard, icon: const Icon(Icons.home_outlined), selectedIcon: const Icon(Icons.home_rounded), label: s.homeTitle, visible: (_) => true),
|
||||
_NavItem(route: routeClinicJobs, icon: const Icon(Icons.work_outline_rounded), selectedIcon: const Icon(Icons.work_rounded), label: s.jobsTitle, visible: (m) => m?.showJobs ?? true),
|
||||
_NavItem(route: routeClinicPatients, icon: const Icon(Icons.people_outline_rounded), selectedIcon: const Icon(Icons.people_rounded), label: s.patientsTitle, visible: (m) => m?.showPatients ?? true),
|
||||
_NavItem(route: routeClinicFinance, icon: const Icon(Icons.account_balance_outlined), selectedIcon: const Icon(Icons.account_balance_rounded), label: s.finance, visible: (m) => m?.showFinance ?? true),
|
||||
_NavItem(route: routeClinicAi, icon: const Icon(Icons.auto_awesome_outlined), selectedIcon: const Icon(Icons.auto_awesome_rounded), label: s.aiAssistant, visible: (_) => true),
|
||||
_NavItem(
|
||||
route: routeClinicDashboard,
|
||||
icon: const Icon(Icons.home_outlined),
|
||||
selectedIcon: const Icon(Icons.home_rounded),
|
||||
label: s.homeTitle,
|
||||
visible: (_) => true),
|
||||
_NavItem(
|
||||
route: routeClinicJobs,
|
||||
icon: const Icon(Icons.work_outline_rounded),
|
||||
selectedIcon: const Icon(Icons.work_rounded),
|
||||
label: s.jobsTitle,
|
||||
visible: (m) => m?.showJobs ?? true),
|
||||
_NavItem(
|
||||
route: routeClinicPatients,
|
||||
icon: const Icon(Icons.people_outline_rounded),
|
||||
selectedIcon: const Icon(Icons.people_rounded),
|
||||
label: s.patientsTitle,
|
||||
visible: (m) => m?.showPatients ?? true),
|
||||
_NavItem(
|
||||
route: routeClinicFinance,
|
||||
icon: const Icon(Icons.account_balance_outlined),
|
||||
selectedIcon: const Icon(Icons.account_balance_rounded),
|
||||
label: s.finance,
|
||||
visible: (m) => m?.showFinance ?? true),
|
||||
_NavItem(
|
||||
route: routeClinicAi,
|
||||
icon: const Icon(Icons.auto_awesome_outlined),
|
||||
selectedIcon: const Icon(Icons.auto_awesome_rounded),
|
||||
label: s.aiAssistant,
|
||||
visible: (_) => true),
|
||||
];
|
||||
|
||||
List<_NavGroup> _clinicGroups(AppStrings s) => [
|
||||
@@ -229,22 +286,62 @@ class _ClinicShellState extends ConsumerState<_ClinicShell> {
|
||||
icon: Icons.tune_rounded,
|
||||
selectedIcon: Icons.tune_rounded,
|
||||
items: [
|
||||
_NavItem(route: routeClinicConnections, icon: const Icon(Icons.link_rounded), selectedIcon: const Icon(Icons.link_rounded), label: s.connections, visible: (_) => true),
|
||||
_NavItem(route: routeClinicReports, icon: const Icon(Icons.bar_chart_outlined), selectedIcon: const Icon(Icons.bar_chart_rounded), label: s.reports, visible: (_) => true),
|
||||
_NavItem(
|
||||
route: routeClinicConnections,
|
||||
icon: const Icon(Icons.link_rounded),
|
||||
selectedIcon: const Icon(Icons.link_rounded),
|
||||
label: s.connections,
|
||||
visible: (_) => true),
|
||||
_NavItem(
|
||||
route: routeClinicReports,
|
||||
icon: const Icon(Icons.bar_chart_outlined),
|
||||
selectedIcon: const Icon(Icons.bar_chart_rounded),
|
||||
label: s.reports,
|
||||
visible: (_) => true),
|
||||
],
|
||||
),
|
||||
];
|
||||
|
||||
List<_NavItem> _clinicBottomSingles(AppStrings s) => [
|
||||
_NavItem(route: routeClinicSettings, icon: const Icon(Icons.settings_outlined), selectedIcon: const Icon(Icons.settings_rounded), label: s.settings, visible: (_) => true),
|
||||
_NavItem(
|
||||
route: routeClinicSettings,
|
||||
icon: const Icon(Icons.settings_outlined),
|
||||
selectedIcon: const Icon(Icons.settings_rounded),
|
||||
label: s.settings,
|
||||
visible: (_) => true),
|
||||
];
|
||||
|
||||
List<_NavItem> _clinicMobileItems(AppStrings s) => [
|
||||
_NavItem(route: routeClinicDashboard, icon: const Icon(Icons.home_outlined), selectedIcon: const Icon(Icons.home_rounded), label: s.homeTitle, visible: (_) => true),
|
||||
_NavItem(route: routeClinicJobs, icon: const Icon(Icons.work_outline_rounded), selectedIcon: const Icon(Icons.work_rounded), label: s.jobsTitle, visible: (m) => m?.showJobs ?? true),
|
||||
_NavItem(route: routeClinicPatients, icon: const Icon(Icons.people_outline_rounded), selectedIcon: const Icon(Icons.people_rounded), label: s.patientsTitle, visible: (m) => m?.showPatients ?? true),
|
||||
_NavItem(route: routeClinicFinance, icon: const Icon(Icons.account_balance_outlined), selectedIcon: const Icon(Icons.account_balance_rounded), label: s.finance, visible: (m) => m?.showFinance ?? true),
|
||||
_NavItem(route: routeClinicSettings, icon: const Icon(Icons.settings_outlined), selectedIcon: const Icon(Icons.settings_rounded), label: s.settings, visible: (_) => true),
|
||||
_NavItem(
|
||||
route: routeClinicDashboard,
|
||||
icon: const Icon(Icons.home_outlined),
|
||||
selectedIcon: const Icon(Icons.home_rounded),
|
||||
label: s.homeTitle,
|
||||
visible: (_) => true),
|
||||
_NavItem(
|
||||
route: routeClinicJobs,
|
||||
icon: const Icon(Icons.work_outline_rounded),
|
||||
selectedIcon: const Icon(Icons.work_rounded),
|
||||
label: s.jobsTitle,
|
||||
visible: (m) => m?.showJobs ?? true),
|
||||
_NavItem(
|
||||
route: routeClinicPatients,
|
||||
icon: const Icon(Icons.people_outline_rounded),
|
||||
selectedIcon: const Icon(Icons.people_rounded),
|
||||
label: s.patientsTitle,
|
||||
visible: (m) => m?.showPatients ?? true),
|
||||
_NavItem(
|
||||
route: routeClinicFinance,
|
||||
icon: const Icon(Icons.account_balance_outlined),
|
||||
selectedIcon: const Icon(Icons.account_balance_rounded),
|
||||
label: s.finance,
|
||||
visible: (m) => m?.showFinance ?? true),
|
||||
_NavItem(
|
||||
route: routeClinicSettings,
|
||||
icon: const Icon(Icons.settings_outlined),
|
||||
selectedIcon: const Icon(Icons.settings_rounded),
|
||||
label: s.settings,
|
||||
visible: (_) => true),
|
||||
];
|
||||
|
||||
List<_SidebarEntry> _allEntries(AppStrings s) {
|
||||
@@ -265,7 +362,8 @@ class _ClinicShellState extends ConsumerState<_ClinicShell> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = ref.watch(stringsProvider);
|
||||
final isDesktop = MediaQuery.sizeOf(context).width > AppLayout.sidebarBreakpoint;
|
||||
final isDesktop =
|
||||
MediaQuery.sizeOf(context).width > AppLayout.sidebarBreakpoint;
|
||||
|
||||
if (isDesktop) {
|
||||
final entries = _allEntries(s);
|
||||
@@ -289,7 +387,8 @@ class _ClinicShellState extends ConsumerState<_ClinicShell> {
|
||||
|
||||
// Mobile: only core items in bottom nav
|
||||
final membership = ref.read(authProvider).activeTenant;
|
||||
final items = _clinicMobileItems(s).where((it) => it.visible(membership)).toList();
|
||||
final items =
|
||||
_clinicMobileItems(s).where((it) => it.visible(membership)).toList();
|
||||
final flatIndex = items.indexWhere((it) => it.route == _selectedRoute);
|
||||
final clampedIndex = flatIndex >= 0 ? flatIndex : 0;
|
||||
|
||||
@@ -317,7 +416,10 @@ class _ClinicShellState extends ConsumerState<_ClinicShell> {
|
||||
Semantics(
|
||||
label: it.label,
|
||||
button: true,
|
||||
child: NavigationDestination(icon: it.icon, selectedIcon: it.selectedIcon, label: it.label),
|
||||
child: NavigationDestination(
|
||||
icon: it.icon,
|
||||
selectedIcon: it.selectedIcon,
|
||||
label: it.label),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -339,11 +441,36 @@ class _LabShellState extends ConsumerState<_LabShell> {
|
||||
String _selectedRoute = routeLabDashboard;
|
||||
|
||||
List<_NavItem> _labTopSingles(AppStrings s) => [
|
||||
_NavItem(route: routeLabDashboard, icon: const Icon(Icons.home_outlined), selectedIcon: const Icon(Icons.home_rounded), label: s.homeTitle, visible: (_) => true),
|
||||
_NavItem(route: routeLabJobsAll, icon: const Icon(Icons.work_outline_rounded), selectedIcon: const Icon(Icons.work_rounded), label: s.jobsTitle, visible: (m) => m?.showJobs ?? true),
|
||||
_NavItem(route: routeLabProducts, icon: const Icon(Icons.inventory_2_outlined), selectedIcon: const Icon(Icons.inventory_2_rounded), label: s.productsTitle, visible: (m) => m?.showProducts ?? true),
|
||||
_NavItem(route: routeLabFinance, icon: const Icon(Icons.account_balance_outlined), selectedIcon: const Icon(Icons.account_balance_rounded), label: s.finance, visible: (m) => m?.showFinance ?? true),
|
||||
_NavItem(route: routeLabAi, icon: const Icon(Icons.auto_awesome_outlined), selectedIcon: const Icon(Icons.auto_awesome_rounded), label: s.aiAssistant, visible: (_) => true),
|
||||
_NavItem(
|
||||
route: routeLabDashboard,
|
||||
icon: const Icon(Icons.home_outlined),
|
||||
selectedIcon: const Icon(Icons.home_rounded),
|
||||
label: s.homeTitle,
|
||||
visible: (_) => true),
|
||||
_NavItem(
|
||||
route: routeLabJobsAll,
|
||||
icon: const Icon(Icons.work_outline_rounded),
|
||||
selectedIcon: const Icon(Icons.work_rounded),
|
||||
label: s.jobsTitle,
|
||||
visible: (m) => m?.showJobs ?? true),
|
||||
_NavItem(
|
||||
route: routeLabProducts,
|
||||
icon: const Icon(Icons.inventory_2_outlined),
|
||||
selectedIcon: const Icon(Icons.inventory_2_rounded),
|
||||
label: s.productsTitle,
|
||||
visible: (m) => m?.showProducts ?? true),
|
||||
_NavItem(
|
||||
route: routeLabFinance,
|
||||
icon: const Icon(Icons.account_balance_outlined),
|
||||
selectedIcon: const Icon(Icons.account_balance_rounded),
|
||||
label: s.finance,
|
||||
visible: (m) => m?.showFinance ?? true),
|
||||
_NavItem(
|
||||
route: routeLabAi,
|
||||
icon: const Icon(Icons.auto_awesome_outlined),
|
||||
selectedIcon: const Icon(Icons.auto_awesome_rounded),
|
||||
label: s.aiAssistant,
|
||||
visible: (_) => true),
|
||||
];
|
||||
|
||||
List<_NavGroup> _labGroups(AppStrings s) => [
|
||||
@@ -352,23 +479,68 @@ class _LabShellState extends ConsumerState<_LabShell> {
|
||||
icon: Icons.tune_rounded,
|
||||
selectedIcon: Icons.tune_rounded,
|
||||
items: [
|
||||
_NavItem(route: routeLabConnections, icon: const Icon(Icons.link_rounded), selectedIcon: const Icon(Icons.link_rounded), label: s.connections, visible: (_) => true),
|
||||
_NavItem(route: routeLabDiscounts, icon: const Icon(Icons.local_offer_outlined), selectedIcon: const Icon(Icons.local_offer_rounded), label: s.discounts, visible: (_) => true),
|
||||
_NavItem(route: routeLabReports, icon: const Icon(Icons.bar_chart_outlined), selectedIcon: const Icon(Icons.bar_chart_rounded), label: s.reports, visible: (_) => true),
|
||||
_NavItem(
|
||||
route: routeLabConnections,
|
||||
icon: const Icon(Icons.link_rounded),
|
||||
selectedIcon: const Icon(Icons.link_rounded),
|
||||
label: s.connections,
|
||||
visible: (_) => true),
|
||||
_NavItem(
|
||||
route: routeLabDiscounts,
|
||||
icon: const Icon(Icons.local_offer_outlined),
|
||||
selectedIcon: const Icon(Icons.local_offer_rounded),
|
||||
label: s.discounts,
|
||||
visible: (_) => true),
|
||||
_NavItem(
|
||||
route: routeLabReports,
|
||||
icon: const Icon(Icons.bar_chart_outlined),
|
||||
selectedIcon: const Icon(Icons.bar_chart_rounded),
|
||||
label: s.reports,
|
||||
visible: (_) => true),
|
||||
],
|
||||
),
|
||||
];
|
||||
|
||||
List<_NavItem> _labBottomSingles(AppStrings s) => [
|
||||
_NavItem(route: routeLabSettings, icon: const Icon(Icons.settings_outlined), selectedIcon: const Icon(Icons.settings_rounded), label: s.settings, visible: (_) => true),
|
||||
_NavItem(
|
||||
route: routeLabSettings,
|
||||
icon: const Icon(Icons.settings_outlined),
|
||||
selectedIcon: const Icon(Icons.settings_rounded),
|
||||
label: s.settings,
|
||||
visible: (_) => true),
|
||||
];
|
||||
|
||||
List<_NavItem> _labMobileItems(AppStrings s) => [
|
||||
_NavItem(route: routeLabDashboard, icon: const Icon(Icons.home_outlined), selectedIcon: const Icon(Icons.home_rounded), label: s.homeTitle, visible: (_) => true),
|
||||
_NavItem(route: routeLabJobsAll, icon: const Icon(Icons.work_outline_rounded), selectedIcon: const Icon(Icons.work_rounded), label: s.jobsTitle, visible: (m) => m?.showJobs ?? true),
|
||||
_NavItem(route: routeLabProducts, icon: const Icon(Icons.inventory_2_outlined), selectedIcon: const Icon(Icons.inventory_2_rounded), label: s.productsTitle, visible: (m) => m?.showProducts ?? true),
|
||||
_NavItem(route: routeLabFinance, icon: const Icon(Icons.account_balance_outlined), selectedIcon: const Icon(Icons.account_balance_rounded), label: s.finance, visible: (m) => m?.showFinance ?? true),
|
||||
_NavItem(route: routeLabSettings, icon: const Icon(Icons.settings_outlined), selectedIcon: const Icon(Icons.settings_rounded), label: s.settings, visible: (_) => true),
|
||||
_NavItem(
|
||||
route: routeLabDashboard,
|
||||
icon: const Icon(Icons.home_outlined),
|
||||
selectedIcon: const Icon(Icons.home_rounded),
|
||||
label: s.homeTitle,
|
||||
visible: (_) => true),
|
||||
_NavItem(
|
||||
route: routeLabJobsAll,
|
||||
icon: const Icon(Icons.work_outline_rounded),
|
||||
selectedIcon: const Icon(Icons.work_rounded),
|
||||
label: s.jobsTitle,
|
||||
visible: (m) => m?.showJobs ?? true),
|
||||
_NavItem(
|
||||
route: routeLabProducts,
|
||||
icon: const Icon(Icons.inventory_2_outlined),
|
||||
selectedIcon: const Icon(Icons.inventory_2_rounded),
|
||||
label: s.productsTitle,
|
||||
visible: (m) => m?.showProducts ?? true),
|
||||
_NavItem(
|
||||
route: routeLabFinance,
|
||||
icon: const Icon(Icons.account_balance_outlined),
|
||||
selectedIcon: const Icon(Icons.account_balance_rounded),
|
||||
label: s.finance,
|
||||
visible: (m) => m?.showFinance ?? true),
|
||||
_NavItem(
|
||||
route: routeLabSettings,
|
||||
icon: const Icon(Icons.settings_outlined),
|
||||
selectedIcon: const Icon(Icons.settings_rounded),
|
||||
label: s.settings,
|
||||
visible: (_) => true),
|
||||
];
|
||||
|
||||
List<_SidebarEntry> _allEntries(AppStrings s) {
|
||||
@@ -389,7 +561,8 @@ class _LabShellState extends ConsumerState<_LabShell> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = ref.watch(stringsProvider);
|
||||
final isDesktop = MediaQuery.sizeOf(context).width > AppLayout.sidebarBreakpoint;
|
||||
final isDesktop =
|
||||
MediaQuery.sizeOf(context).width > AppLayout.sidebarBreakpoint;
|
||||
|
||||
if (isDesktop) {
|
||||
final entries = _allEntries(s);
|
||||
@@ -413,7 +586,8 @@ class _LabShellState extends ConsumerState<_LabShell> {
|
||||
|
||||
// Mobile: only core items in bottom nav
|
||||
final membership = ref.read(authProvider).activeTenant;
|
||||
final items = _labMobileItems(s).where((it) => it.visible(membership)).toList();
|
||||
final items =
|
||||
_labMobileItems(s).where((it) => it.visible(membership)).toList();
|
||||
final flatIndex = items.indexWhere((it) => it.route == _selectedRoute);
|
||||
final clampedIndex = flatIndex >= 0 ? flatIndex : 0;
|
||||
|
||||
@@ -441,7 +615,10 @@ class _LabShellState extends ConsumerState<_LabShell> {
|
||||
Semantics(
|
||||
label: it.label,
|
||||
button: true,
|
||||
child: NavigationDestination(icon: it.icon, selectedIcon: it.selectedIcon, label: it.label),
|
||||
child: NavigationDestination(
|
||||
icon: it.icon,
|
||||
selectedIcon: it.selectedIcon,
|
||||
label: it.label),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -483,7 +660,10 @@ class _DesktopSidebarState extends State<_DesktopSidebar> {
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
border: Border(right: BorderSide(color: AppColors.border)),
|
||||
boxShadow: [BoxShadow(color: Color(0x08000000), blurRadius: 8, offset: Offset(2, 0))],
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Color(0x08000000), blurRadius: 8, offset: Offset(2, 0))
|
||||
],
|
||||
),
|
||||
child: ClipRect(
|
||||
child: Column(
|
||||
@@ -492,7 +672,8 @@ class _DesktopSidebarState extends State<_DesktopSidebar> {
|
||||
Container(
|
||||
height: _DesktopSidebar.headerHeight,
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(colors: [AppColors.primary, AppColors.accent]),
|
||||
gradient: LinearGradient(
|
||||
colors: [AppColors.primary, AppColors.accent]),
|
||||
border: Border(bottom: BorderSide(color: AppColors.border)),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
@@ -504,15 +685,21 @@ class _DesktopSidebarState extends State<_DesktopSidebar> {
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(9),
|
||||
border: Border.all(color: Colors.white.withValues(alpha: 0.25)),
|
||||
border: Border.all(
|
||||
color: Colors.white.withValues(alpha: 0.25)),
|
||||
),
|
||||
child: const Center(child: ToothLogo(size: 18, color: Colors.white)),
|
||||
child: const Center(
|
||||
child: ToothLogo(size: 18, color: Colors.white)),
|
||||
),
|
||||
if (_open) ...[
|
||||
const SizedBox(width: 10),
|
||||
const Text(
|
||||
'DLS',
|
||||
style: TextStyle(color: Colors.white, fontSize: 17, fontWeight: FontWeight.w800, letterSpacing: 1),
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: 1),
|
||||
),
|
||||
],
|
||||
],
|
||||
@@ -525,8 +712,7 @@ class _DesktopSidebarState extends State<_DesktopSidebar> {
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
for (final entry in widget.entries)
|
||||
_buildEntry(entry),
|
||||
for (final entry in widget.entries) _buildEntry(entry),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
@@ -545,17 +731,24 @@ class _DesktopSidebarState extends State<_DesktopSidebar> {
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
child: Row(
|
||||
mainAxisAlignment: _open ? MainAxisAlignment.start : MainAxisAlignment.center,
|
||||
mainAxisAlignment: _open
|
||||
? MainAxisAlignment.start
|
||||
: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (_open) const SizedBox(width: 20),
|
||||
AnimatedRotation(
|
||||
duration: const Duration(milliseconds: 220),
|
||||
turns: _open ? 0.5 : 0,
|
||||
child: const Icon(Icons.chevron_right_rounded, color: AppColors.textMuted, size: 20),
|
||||
child: const Icon(Icons.chevron_right_rounded,
|
||||
color: AppColors.textMuted, size: 20),
|
||||
),
|
||||
if (_open) ...[
|
||||
const SizedBox(width: 8),
|
||||
const Text('Daralt', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: AppColors.textMuted)),
|
||||
const Text('Daralt',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.textMuted)),
|
||||
],
|
||||
],
|
||||
),
|
||||
@@ -629,7 +822,9 @@ class _SidebarItem extends StatelessWidget {
|
||||
children: [
|
||||
IconTheme(
|
||||
data: IconThemeData(
|
||||
color: selected ? AppColors.primary : AppColors.textSecondary,
|
||||
color: selected
|
||||
? AppColors.primary
|
||||
: AppColors.textSecondary,
|
||||
size: 20,
|
||||
),
|
||||
child: selected ? selectedIcon : icon,
|
||||
@@ -639,8 +834,11 @@ class _SidebarItem extends StatelessWidget {
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: selected ? FontWeight.w600 : FontWeight.w500,
|
||||
color: selected ? AppColors.primary : AppColors.textSecondary,
|
||||
fontWeight:
|
||||
selected ? FontWeight.w600 : FontWeight.w500,
|
||||
color: selected
|
||||
? AppColors.primary
|
||||
: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -649,7 +847,9 @@ class _SidebarItem extends StatelessWidget {
|
||||
: Center(
|
||||
child: IconTheme(
|
||||
data: IconThemeData(
|
||||
color: selected ? AppColors.primary : AppColors.textSecondary,
|
||||
color: selected
|
||||
? AppColors.primary
|
||||
: AppColors.textSecondary,
|
||||
size: 20,
|
||||
),
|
||||
child: selected ? selectedIcon : icon,
|
||||
@@ -740,7 +940,9 @@ class _SidebarGroupState extends State<_SidebarGroup> {
|
||||
child: Icon(
|
||||
isSelected ? widget.group.selectedIcon : widget.group.icon,
|
||||
size: 20,
|
||||
color: isSelected ? AppColors.primary : AppColors.textSecondary,
|
||||
color: isSelected
|
||||
? AppColors.primary
|
||||
: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -768,20 +970,24 @@ class _SidebarGroupState extends State<_SidebarGroup> {
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
isSelected ? widget.group.selectedIcon : widget.group.icon,
|
||||
isSelected
|
||||
? widget.group.selectedIcon
|
||||
: widget.group.icon,
|
||||
size: 20,
|
||||
color: isSelected ? AppColors.primary : AppColors.textSecondary,
|
||||
color: isSelected
|
||||
? AppColors.primary
|
||||
: AppColors.textSecondary,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.group.title,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
child: Text(
|
||||
widget.group.title,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedRotation(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
@@ -803,7 +1009,8 @@ class _SidebarGroupState extends State<_SidebarGroup> {
|
||||
// Sub-items (animated expand/collapse)
|
||||
AnimatedCrossFade(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
crossFadeState: _expanded ? CrossFadeState.showFirst : CrossFadeState.showSecond,
|
||||
crossFadeState:
|
||||
_expanded ? CrossFadeState.showFirst : CrossFadeState.showSecond,
|
||||
firstChild: Column(
|
||||
children: [
|
||||
for (final item in widget.group.items)
|
||||
|
||||
@@ -17,18 +17,19 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
|
||||
return GoRouter(
|
||||
refreshListenable: notifier,
|
||||
initialLocation: routeSignIn,
|
||||
initialLocation: routeWelcome,
|
||||
redirect: (context, state) {
|
||||
final auth = ref.read(authProvider);
|
||||
|
||||
if (auth.isLoading) return null;
|
||||
|
||||
final loc = state.matchedLocation;
|
||||
final onLoginOrRegister = loc == routeSignIn || loc == routeSignUp;
|
||||
final onLoginOrRegister =
|
||||
loc == routeSignIn || loc == routeSignUp || loc == routeWelcome;
|
||||
final onAuthPage = onLoginOrRegister || loc == routeOnboarding;
|
||||
|
||||
if (!auth.isAuthenticated) {
|
||||
return onAuthPage ? null : routeSignIn;
|
||||
return onAuthPage ? null : routeWelcome;
|
||||
}
|
||||
|
||||
// Authenticated but no tenant → onboarding
|
||||
|
||||
@@ -20,9 +20,9 @@ class FinanceService {
|
||||
if (amount <= 0) return;
|
||||
|
||||
final existing = await _pb.collection('finance_entries').getFullList(
|
||||
filter: 'job_id = "$jobId"',
|
||||
batch: 200,
|
||||
);
|
||||
filter: 'job_id = "$jobId"',
|
||||
batch: 200,
|
||||
);
|
||||
|
||||
await _upsertEntry(
|
||||
existing: existing,
|
||||
@@ -47,11 +47,27 @@ class FinanceService {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> markJobPaid(String jobId) async {
|
||||
Future<void> reportJobPayment(String jobId) async {
|
||||
final existing = await _pb.collection('finance_entries').getFullList(
|
||||
filter: 'job_id = "$jobId"',
|
||||
batch: 200,
|
||||
);
|
||||
filter: 'job_id = "$jobId"',
|
||||
batch: 200,
|
||||
);
|
||||
for (final record in existing) {
|
||||
await _pb.collection('finance_entries').update(
|
||||
record.id,
|
||||
body: {
|
||||
'status': 'reported',
|
||||
'paid_at': null,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> confirmJobPayment(String jobId) async {
|
||||
final existing = await _pb.collection('finance_entries').getFullList(
|
||||
filter: 'job_id = "$jobId"',
|
||||
batch: 200,
|
||||
);
|
||||
final paidAt = DateTime.now().toIso8601String();
|
||||
for (final record in existing) {
|
||||
await _pb.collection('finance_entries').update(
|
||||
@@ -66,9 +82,10 @@ class FinanceService {
|
||||
|
||||
Future<void> deletePendingEntriesForJob(String jobId) async {
|
||||
final existing = await _pb.collection('finance_entries').getFullList(
|
||||
filter: 'job_id = "$jobId" && status = "pending"',
|
||||
batch: 200,
|
||||
);
|
||||
filter:
|
||||
'job_id = "$jobId" && (status = "pending" || status = "reported")',
|
||||
batch: 200,
|
||||
);
|
||||
for (final record in existing) {
|
||||
await _pb.collection('finance_entries').delete(record.id);
|
||||
}
|
||||
@@ -88,8 +105,7 @@ class FinanceService {
|
||||
try {
|
||||
match = existing.firstWhere(
|
||||
(record) =>
|
||||
record.data['tenant_id'] == tenantId &&
|
||||
record.data['type'] == type,
|
||||
record.data['tenant_id'] == tenantId && record.data['type'] == type,
|
||||
);
|
||||
} catch (_) {
|
||||
match = null;
|
||||
|
||||
@@ -19,6 +19,7 @@ class JobHistoryEntry {
|
||||
|
||||
enum JobHistoryAction {
|
||||
accepted,
|
||||
stepCompleted,
|
||||
handedToClinic,
|
||||
approved,
|
||||
revisionRequested,
|
||||
@@ -29,6 +30,7 @@ enum JobHistoryAction {
|
||||
extension JobHistoryActionExt on JobHistoryAction {
|
||||
String get value => switch (this) {
|
||||
JobHistoryAction.accepted => 'accepted',
|
||||
JobHistoryAction.stepCompleted => 'step_completed',
|
||||
JobHistoryAction.handedToClinic => 'handed_to_clinic',
|
||||
JobHistoryAction.approved => 'approved',
|
||||
JobHistoryAction.revisionRequested => 'revision_requested',
|
||||
@@ -43,21 +45,21 @@ class JobHistoryService {
|
||||
|
||||
PocketBase get _pb => PocketBaseClient.instance.pb;
|
||||
|
||||
String get _currentUserId =>
|
||||
(_pb.authStore.record?.id) ?? (_pb.authStore.model as dynamic)?.id as String? ?? '';
|
||||
String get _currentUserId => _pb.authStore.record?.id ?? '';
|
||||
|
||||
Future<List<JobHistoryEntry>> listForJob(String jobId) async {
|
||||
try {
|
||||
final result = await _pb.collection('job_status_history').getList(
|
||||
filter: 'job_id = "$jobId"',
|
||||
perPage: 200,
|
||||
);
|
||||
filter: 'job_id = "$jobId"',
|
||||
perPage: 200,
|
||||
);
|
||||
return (result.items.map((r) {
|
||||
final j = r.toJson();
|
||||
String? str(dynamic v) {
|
||||
final s = v as String?;
|
||||
return (s == null || s.isEmpty) ? null : s;
|
||||
}
|
||||
|
||||
return JobHistoryEntry(
|
||||
id: j['id'] as String,
|
||||
action: _parseAction(j['action_type'] as String? ?? ''),
|
||||
@@ -65,30 +67,38 @@ class JobHistoryService {
|
||||
note: str(j['note']),
|
||||
createdAt: DateTime.parse(j['created'] as String),
|
||||
);
|
||||
}).toList()..sort((a, b) => a.createdAt.compareTo(b.createdAt)));
|
||||
}).toList()
|
||||
..sort((a, b) => a.createdAt.compareTo(b.createdAt)));
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
static JobHistoryAction _parseAction(String s) => switch (s) {
|
||||
'accepted' => JobHistoryAction.accepted,
|
||||
'handed_to_clinic' => JobHistoryAction.handedToClinic,
|
||||
'approved' => JobHistoryAction.approved,
|
||||
'accepted' => JobHistoryAction.accepted,
|
||||
'step_completed' => JobHistoryAction.stepCompleted,
|
||||
'handed_to_clinic' => JobHistoryAction.handedToClinic,
|
||||
'approved' => JobHistoryAction.approved,
|
||||
'revision_requested' => JobHistoryAction.revisionRequested,
|
||||
'delivered' => JobHistoryAction.delivered,
|
||||
_ => JobHistoryAction.cancelled,
|
||||
'delivered' => JobHistoryAction.delivered,
|
||||
_ => JobHistoryAction.cancelled,
|
||||
};
|
||||
|
||||
static JobStep _parseStep(String s) => switch (s) {
|
||||
'olcu_kontrol' => JobStep.olcuKontrol,
|
||||
'dijital_tasarim' => JobStep.dijitalTasarim,
|
||||
'model_hazirlik' => JobStep.modelHazirlik,
|
||||
'alt_yapi_prova' => JobStep.altYapiProva,
|
||||
'ust_yapi_prova' => JobStep.ustYapiProva,
|
||||
'mum_prova' => JobStep.mumProva,
|
||||
'disler_prova' => JobStep.dislerProva,
|
||||
'dayanak_prova' => JobStep.dayanakProva,
|
||||
'kron_prova' => JobStep.kronProva,
|
||||
'cila_bitim' => JobStep.cilaBitim,
|
||||
_ => JobStep.olcu,
|
||||
'mum_prova' => JobStep.mumProva,
|
||||
'disler_prova' => JobStep.dislerProva,
|
||||
'dayanak_prova' => JobStep.dayanakProva,
|
||||
'kron_prova' => JobStep.kronProva,
|
||||
'fotograf_onay' => JobStep.fotografOnay,
|
||||
'kalite_kontrol' => JobStep.kaliteKontrol,
|
||||
'teslim_oncesi_kontrol' => JobStep.teslimOncesiKontrol,
|
||||
'cila_bitim' => JobStep.cilaBitim,
|
||||
_ => JobStep.olcu,
|
||||
};
|
||||
|
||||
Future<void> append({
|
||||
|
||||
@@ -16,21 +16,33 @@ class RealtimeService {
|
||||
required void Function(RecordSubscriptionEvent) onEvent,
|
||||
}) {
|
||||
UnsubFn? cancel;
|
||||
bool disposeRequested = false;
|
||||
bool disposed = false;
|
||||
|
||||
_pb.collection(collection).subscribe(topic, onEvent, filter: filter).then((fn) {
|
||||
_pb
|
||||
.collection(collection)
|
||||
.subscribe(topic, onEvent, filter: filter)
|
||||
.then((fn) async {
|
||||
if (disposeRequested) {
|
||||
disposed = true;
|
||||
await fn();
|
||||
return;
|
||||
}
|
||||
cancel = fn;
|
||||
});
|
||||
}).catchError((_) {});
|
||||
|
||||
return () async {
|
||||
if (disposed) return;
|
||||
disposeRequested = true;
|
||||
try {
|
||||
final fn = cancel;
|
||||
if (fn != null) {
|
||||
disposed = true;
|
||||
await fn();
|
||||
} else {
|
||||
await _pb.collection(collection).unsubscribe(topic);
|
||||
}
|
||||
} catch (_) {
|
||||
await _pb.collection(collection).unsubscribe(topic);
|
||||
// swallow — never globally unsubscribe the topic here, because
|
||||
// other screens may still be subscribed to the same collection/topic.
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user