Initial commit — DLS lab-app Flutter project
This commit is contained in:
@@ -0,0 +1,496 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
import '../widgets/tooth_logo.dart';
|
||||
import '../providers/auth_provider.dart';
|
||||
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/clinic/dashboard/clinic_dashboard_screen.dart';
|
||||
import '../../features/clinic/jobs/clinic_jobs_screen.dart';
|
||||
import '../../features/clinic/jobs/clinic_job_detail_screen.dart';
|
||||
import '../../features/clinic/jobs/new_job_screen.dart';
|
||||
import '../../features/clinic/patients/clinic_patients_screen.dart';
|
||||
import '../../features/clinic/patients/clinic_patient_detail_screen.dart';
|
||||
import '../../features/clinic/connections/clinic_connections_screen.dart';
|
||||
import '../../features/clinic/finance/clinic_finance_screen.dart';
|
||||
import '../../features/clinic/settings/clinic_settings_screen.dart';
|
||||
import '../../features/lab/dashboard/lab_dashboard_screen.dart';
|
||||
import '../../features/lab/jobs/lab_jobs_inbound_screen.dart';
|
||||
import '../../features/lab/jobs/lab_all_jobs_screen.dart';
|
||||
import '../../features/lab/jobs/lab_job_detail_screen.dart';
|
||||
import '../../features/lab/products/lab_products_screen.dart';
|
||||
import '../../features/lab/connections/lab_connections_screen.dart';
|
||||
import '../../features/lab/finance/lab_finance_screen.dart';
|
||||
import '../../features/lab/settings/lab_settings_screen.dart';
|
||||
import '../../features/shared/reports_screen.dart';
|
||||
import '../../features/shared/ai_chat_screen.dart';
|
||||
import '../../features/lab/discounts/discounts_screen.dart';
|
||||
import '../../features/lab/connections/connection_detail_screen.dart';
|
||||
import '../../models/connection.dart';
|
||||
|
||||
// Auth routes
|
||||
const routeSignIn = '/sign-in';
|
||||
const routeSignUp = '/sign-up';
|
||||
const routeOnboarding = '/onboarding';
|
||||
|
||||
// Clinic routes
|
||||
const routeClinicDashboard = '/clinic/dashboard';
|
||||
const routeClinicJobs = '/clinic/jobs';
|
||||
const routeClinicJobDetail = '/clinic/jobs/:jobId';
|
||||
const routeClinicJobNew = '/clinic/jobs/new';
|
||||
const routeClinicPatients = '/clinic/patients';
|
||||
const routeClinicPatientDetail = '/clinic/patients/:patientId';
|
||||
const routeClinicConnections = '/clinic/connections';
|
||||
const routeClinicFinance = '/clinic/finance';
|
||||
const routeClinicSettings = '/clinic/settings';
|
||||
const routeClinicReports = '/clinic/reports';
|
||||
const routeClinicAi = '/clinic/ai';
|
||||
|
||||
// Lab routes
|
||||
const routeLabDashboard = '/lab/dashboard';
|
||||
const routeLabJobsInbound = '/lab/jobs/inbound';
|
||||
const routeLabJobsAll = '/lab/jobs';
|
||||
const routeLabJobDetail = '/lab/jobs/:jobId';
|
||||
const routeLabProducts = '/lab/products';
|
||||
const routeLabConnections = '/lab/connections';
|
||||
const routeLabFinance = '/lab/finance';
|
||||
const routeLabSettings = '/lab/settings';
|
||||
const routeLabReports = '/lab/reports';
|
||||
const routeLabAi = '/lab/ai';
|
||||
const routeLabDiscounts = '/lab/discounts';
|
||||
|
||||
List<RouteBase> buildRoutes() => [
|
||||
GoRoute(path: routeSignIn, builder: (_, __) => const SignInScreen()),
|
||||
GoRoute(path: routeSignUp, builder: (_, __) => const SignUpScreen()),
|
||||
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: routeClinicJobs,
|
||||
builder: (_, __) => const ClinicJobsScreen(),
|
||||
routes: [
|
||||
GoRoute(path: 'new', builder: (_, __) => const NewJobScreen()),
|
||||
GoRoute(
|
||||
path: ':jobId',
|
||||
builder: (_, s) => ClinicJobDetailScreen(jobId: s.pathParameters['jobId']!),
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: routeClinicPatients,
|
||||
builder: (_, __) => const ClinicPatientsScreen(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: ':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()),
|
||||
],
|
||||
),
|
||||
|
||||
// ── Lab shell ─────────────────────────────────────────────────────────
|
||||
ShellRoute(
|
||||
builder: (context, state, child) => _LabShell(child: child),
|
||||
routes: [
|
||||
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']!),
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(path: routeLabProducts, builder: (_, __) => const LabProductsScreen()),
|
||||
GoRoute(
|
||||
path: routeLabConnections,
|
||||
builder: (_, __) => const LabConnectionsScreen(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: ':connectionId/detail',
|
||||
builder: (_, s) {
|
||||
final extra = s.extra as Map<String, dynamic>?;
|
||||
final connection = extra?['connection'] as Connection?;
|
||||
final labTenantId = extra?['labTenantId'] as String? ?? '';
|
||||
if (connection == null) {
|
||||
return const Scaffold(
|
||||
body: Center(child: Text('Bağlantı bulunamadı')),
|
||||
);
|
||||
}
|
||||
return ConnectionDetailScreen(
|
||||
connection: connection, labTenantId: labTenantId);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
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()),
|
||||
],
|
||||
),
|
||||
];
|
||||
|
||||
// ── Nav item descriptor ───────────────────────────────────────────────────────
|
||||
|
||||
class _NavItem {
|
||||
const _NavItem({
|
||||
required this.route,
|
||||
required this.icon,
|
||||
required this.selectedIcon,
|
||||
required this.label,
|
||||
required this.visible,
|
||||
});
|
||||
final String route;
|
||||
final Icon icon;
|
||||
final Icon selectedIcon;
|
||||
final String label;
|
||||
final bool Function(TenantMembership?) visible;
|
||||
}
|
||||
|
||||
// ── Clinic shell ──────────────────────────────────────────────────────────────
|
||||
|
||||
class _ClinicShell extends ConsumerStatefulWidget {
|
||||
const _ClinicShell({required this.child});
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
ConsumerState<_ClinicShell> createState() => _ClinicShellState();
|
||||
}
|
||||
|
||||
class _ClinicShellState extends ConsumerState<_ClinicShell> {
|
||||
int _index = 0;
|
||||
|
||||
static final _allItems = [
|
||||
_NavItem(route: routeClinicDashboard, icon: const Icon(Icons.home_outlined), selectedIcon: const Icon(Icons.home_rounded), label: 'Ana Sayfa', visible: (_) => true),
|
||||
_NavItem(route: routeClinicJobs, icon: const Icon(Icons.work_outline_rounded), selectedIcon: const Icon(Icons.work_rounded), label: 'İşler', visible: (m) => m?.showJobs ?? true),
|
||||
_NavItem(route: routeClinicPatients, icon: const Icon(Icons.people_outline_rounded), selectedIcon: const Icon(Icons.people_rounded), label: 'Hastalar', visible: (m) => m?.showPatients ?? true),
|
||||
_NavItem(route: routeClinicFinance, icon: const Icon(Icons.account_balance_outlined), selectedIcon: const Icon(Icons.account_balance_rounded), label: 'Finans', visible: (m) => m?.showFinance ?? true),
|
||||
_NavItem(route: routeClinicSettings, icon: const Icon(Icons.settings_outlined), selectedIcon: const Icon(Icons.settings_rounded), label: 'Ayarlar', visible: (_) => true),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final membership = ref.watch(authProvider).activeTenant;
|
||||
final items = _allItems.where((it) => it.visible(membership)).toList();
|
||||
final clampedIndex = _index.clamp(0, items.length - 1);
|
||||
final isDesktop = MediaQuery.sizeOf(context).width > AppLayout.sidebarBreakpoint;
|
||||
|
||||
void onTap(int i) {
|
||||
setState(() => _index = i);
|
||||
context.go(items[i].route);
|
||||
}
|
||||
|
||||
if (isDesktop) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.background,
|
||||
body: Row(
|
||||
children: [
|
||||
_DesktopSidebar(destinations: items, selectedIndex: clampedIndex, onTap: onTap),
|
||||
Expanded(child: widget.child),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
body: widget.child,
|
||||
bottomNavigationBar: NavigationBar(
|
||||
selectedIndex: clampedIndex,
|
||||
onDestinationSelected: onTap,
|
||||
destinations: [
|
||||
for (final it in items)
|
||||
Semantics(
|
||||
label: it.label,
|
||||
button: true,
|
||||
child: NavigationDestination(icon: it.icon, selectedIcon: it.selectedIcon, label: it.label),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Lab shell ─────────────────────────────────────────────────────────────────
|
||||
|
||||
class _LabShell extends ConsumerStatefulWidget {
|
||||
const _LabShell({required this.child});
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
ConsumerState<_LabShell> createState() => _LabShellState();
|
||||
}
|
||||
|
||||
class _LabShellState extends ConsumerState<_LabShell> {
|
||||
int _index = 0;
|
||||
|
||||
static final _allItems = [
|
||||
_NavItem(route: routeLabDashboard, icon: const Icon(Icons.home_outlined), selectedIcon: const Icon(Icons.home_rounded), label: 'Ana Sayfa', visible: (_) => true),
|
||||
_NavItem(route: routeLabJobsAll, icon: const Icon(Icons.work_outline_rounded), selectedIcon: const Icon(Icons.work_rounded), label: 'İşler', visible: (m) => m?.showJobs ?? true),
|
||||
_NavItem(route: routeLabProducts, icon: const Icon(Icons.inventory_2_outlined), selectedIcon: const Icon(Icons.inventory_2_rounded), label: 'Ürünler', visible: (m) => m?.showProducts ?? true),
|
||||
_NavItem(route: routeLabFinance, icon: const Icon(Icons.account_balance_outlined), selectedIcon: const Icon(Icons.account_balance_rounded), label: 'Finans', visible: (m) => m?.showFinance ?? true),
|
||||
_NavItem(route: routeLabSettings, icon: const Icon(Icons.settings_outlined), selectedIcon: const Icon(Icons.settings_rounded), label: 'Ayarlar', visible: (_) => true),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final membership = ref.watch(authProvider).activeTenant;
|
||||
final items = _allItems.where((it) => it.visible(membership)).toList();
|
||||
final clampedIndex = _index.clamp(0, items.length - 1);
|
||||
final isDesktop = MediaQuery.sizeOf(context).width > AppLayout.sidebarBreakpoint;
|
||||
|
||||
void onTap(int i) {
|
||||
setState(() => _index = i);
|
||||
context.go(items[i].route);
|
||||
}
|
||||
|
||||
if (isDesktop) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.background,
|
||||
body: Row(
|
||||
children: [
|
||||
_DesktopSidebar(destinations: items, selectedIndex: clampedIndex, onTap: onTap),
|
||||
Expanded(child: widget.child),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
body: widget.child,
|
||||
bottomNavigationBar: NavigationBar(
|
||||
selectedIndex: clampedIndex,
|
||||
onDestinationSelected: onTap,
|
||||
destinations: [
|
||||
for (final it in items)
|
||||
Semantics(
|
||||
label: it.label,
|
||||
button: true,
|
||||
child: NavigationDestination(icon: it.icon, selectedIcon: it.selectedIcon, label: it.label),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Desktop sidebar ───────────────────────────────────────────────────────────
|
||||
|
||||
class _DesktopSidebar extends StatefulWidget {
|
||||
const _DesktopSidebar({
|
||||
required this.destinations,
|
||||
required this.selectedIndex,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final List<_NavItem> destinations;
|
||||
final int selectedIndex;
|
||||
final ValueChanged<int> onTap;
|
||||
|
||||
// Must match the toolbarHeight used in desktop SliverAppBar headers
|
||||
static const double headerHeight = 64;
|
||||
static const double _openWidth = 220;
|
||||
static const double _closedWidth = 64;
|
||||
|
||||
@override
|
||||
State<_DesktopSidebar> createState() => _DesktopSidebarState();
|
||||
}
|
||||
|
||||
class _DesktopSidebarState extends State<_DesktopSidebar> {
|
||||
bool _open = true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 220),
|
||||
curve: Curves.easeInOut,
|
||||
width: _open ? _DesktopSidebar._openWidth : _DesktopSidebar._closedWidth,
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
border: Border(right: BorderSide(color: AppColors.border)),
|
||||
boxShadow: [BoxShadow(color: Color(0x08000000), blurRadius: 8, offset: Offset(2, 0))],
|
||||
),
|
||||
child: ClipRect(
|
||||
child: Column(
|
||||
children: [
|
||||
// Header
|
||||
Container(
|
||||
height: _DesktopSidebar.headerHeight,
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(colors: [AppColors.primary, AppColors.accent]),
|
||||
border: Border(bottom: BorderSide(color: AppColors.border)),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(9),
|
||||
border: Border.all(color: Colors.white.withValues(alpha: 0.25)),
|
||||
),
|
||||
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),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Nav items
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
for (int i = 0; i < widget.destinations.length; i++)
|
||||
_SidebarItem(
|
||||
icon: widget.destinations[i].icon,
|
||||
selectedIcon: widget.destinations[i].selectedIcon,
|
||||
label: widget.destinations[i].label,
|
||||
selected: widget.selectedIndex == i,
|
||||
open: _open,
|
||||
onTap: () => widget.onTap(i),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Toggle button
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(top: BorderSide(color: AppColors.border)),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () => setState(() => _open = !_open),
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
child: Row(
|
||||
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),
|
||||
),
|
||||
if (_open) ...[
|
||||
const SizedBox(width: 8),
|
||||
const Text('Daralt', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: AppColors.textMuted)),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Sidebar nav item ──────────────────────────────────────────────────────────
|
||||
|
||||
class _SidebarItem extends StatelessWidget {
|
||||
const _SidebarItem({
|
||||
required this.icon,
|
||||
required this.selectedIcon,
|
||||
required this.label,
|
||||
required this.selected,
|
||||
required this.open,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final Widget icon;
|
||||
final Widget selectedIcon;
|
||||
final String label;
|
||||
final bool selected;
|
||||
final bool open;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final item = Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
child: Material(
|
||||
color: selected ? const Color(0xFFDBEAFE) : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: open
|
||||
? Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
IconTheme(
|
||||
data: IconThemeData(
|
||||
color: selected ? AppColors.primary : AppColors.textSecondary,
|
||||
size: 20,
|
||||
),
|
||||
child: selected ? selectedIcon : icon,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: selected ? FontWeight.w600 : FontWeight.w500,
|
||||
color: selected ? AppColors.primary : AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: Center(
|
||||
child: IconTheme(
|
||||
data: IconThemeData(
|
||||
color: selected ? AppColors.primary : AppColors.textSecondary,
|
||||
size: 20,
|
||||
),
|
||||
child: selected ? selectedIcon : icon,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (!open) {
|
||||
return Tooltip(message: label, preferBelow: false, child: item);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../providers/auth_provider.dart';
|
||||
import 'app_router.dart';
|
||||
|
||||
// Bridges Riverpod auth state changes to GoRouter's Listenable interface
|
||||
class _AuthRouterNotifier extends ChangeNotifier {
|
||||
_AuthRouterNotifier(this._ref) {
|
||||
_ref.listen<AuthState>(authProvider, (_, __) => notifyListeners());
|
||||
}
|
||||
final Ref _ref;
|
||||
}
|
||||
|
||||
final routerProvider = Provider<GoRouter>((ref) {
|
||||
final notifier = _AuthRouterNotifier(ref);
|
||||
|
||||
return GoRouter(
|
||||
refreshListenable: notifier,
|
||||
initialLocation: routeSignIn,
|
||||
redirect: (context, state) {
|
||||
final auth = ref.read(authProvider);
|
||||
|
||||
if (auth.isLoading) return null;
|
||||
|
||||
final loc = state.matchedLocation;
|
||||
final onLoginOrRegister = loc == routeSignIn || loc == routeSignUp;
|
||||
final onAuthPage = onLoginOrRegister || loc == routeOnboarding;
|
||||
|
||||
if (!auth.isAuthenticated) {
|
||||
return onAuthPage ? null : routeSignIn;
|
||||
}
|
||||
|
||||
// Authenticated but no tenant → onboarding
|
||||
if (auth.activeTenant == null) {
|
||||
return loc == routeOnboarding ? null : routeOnboarding;
|
||||
}
|
||||
|
||||
final isLab = auth.activeTenant!.tenant.isLab;
|
||||
|
||||
if (onAuthPage) {
|
||||
return isLab ? routeLabDashboard : routeClinicDashboard;
|
||||
}
|
||||
|
||||
if (isLab && loc.startsWith('/clinic')) return routeLabDashboard;
|
||||
if (!isLab && loc.startsWith('/lab')) return routeClinicDashboard;
|
||||
|
||||
return null;
|
||||
},
|
||||
routes: buildRoutes(),
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user