Initial commit — DLS lab-app Flutter project
This commit is contained in:
@@ -0,0 +1,785 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../../core/l10n/app_strings.dart';
|
||||
import '../../../core/providers/auth_provider.dart';
|
||||
import '../../../core/providers/locale_provider.dart';
|
||||
import '../../../core/router/app_router.dart';
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
import '../../../models/tenant.dart';
|
||||
import '../../shared/tenant_team_screen.dart';
|
||||
import '../connections/lab_connections_screen.dart';
|
||||
|
||||
class LabSettingsScreen extends ConsumerWidget {
|
||||
const LabSettingsScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final auth = ref.watch(authProvider);
|
||||
final s = ref.watch(stringsProvider);
|
||||
final profile = auth.profile;
|
||||
final membership = auth.activeTenant;
|
||||
final tenant = membership?.tenant;
|
||||
final canEdit = membership?.isAdmin ?? false;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(s.settings)),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
// User card
|
||||
_SectionHeader(title: s.userInfo),
|
||||
_UserCard(profile: profile),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Lab info
|
||||
_SectionHeader(
|
||||
title: s.labInfo,
|
||||
action: canEdit
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.edit_outlined,
|
||||
size: 18, color: AppColors.accent),
|
||||
tooltip: s.edit,
|
||||
onPressed: () => _showEditSheet(context, ref, tenant, s),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
_InfoCard(children: [
|
||||
_InfoTile(
|
||||
icon: Icons.science_outlined,
|
||||
label: s.labName,
|
||||
value: tenant?.companyName ?? '-',
|
||||
),
|
||||
_InfoTile(
|
||||
icon: Icons.payments_outlined,
|
||||
label: s.currency,
|
||||
value: tenant?.defaultCurrency ?? 'TRY',
|
||||
),
|
||||
_InfoTileBadge(
|
||||
icon: Icons.circle_outlined,
|
||||
label: s.status,
|
||||
value: tenant?.status == 'active' ? s.active : (tenant?.status ?? '-'),
|
||||
badgeColor: AppColors.success,
|
||||
badgeBg: AppColors.successBg,
|
||||
),
|
||||
_InfoTile(
|
||||
icon: Icons.star_outline,
|
||||
label: s.role,
|
||||
value: _roleLabel(membership?.role, s),
|
||||
),
|
||||
]),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Connections
|
||||
if (membership?.showConnections ?? false) ...[
|
||||
_SectionHeader(title: s.connections),
|
||||
_InfoCard(children: [
|
||||
_NavTile(
|
||||
icon: Icons.link_rounded,
|
||||
iconColor: AppColors.inProgress,
|
||||
iconBg: AppColors.inProgressBg,
|
||||
title: s.clinicConnections,
|
||||
subtitle: s.clinicConnectionsSub,
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const LabConnectionsScreen()),
|
||||
),
|
||||
),
|
||||
]),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
|
||||
// Other memberships
|
||||
if (auth.memberships.length > 1) ...[
|
||||
_SectionHeader(title: s.otherMemberships),
|
||||
_InfoCard(children: [
|
||||
for (final m
|
||||
in auth.memberships.where((m) => m.id != membership?.id))
|
||||
_NavTile(
|
||||
icon: Icons.switch_account_outlined,
|
||||
iconColor: AppColors.inProgress,
|
||||
iconBg: AppColors.inProgressBg,
|
||||
title: m.tenant.companyName,
|
||||
subtitle: _tenantKindLabel(m.tenant.kind, s),
|
||||
onTap: () {
|
||||
ref.read(authProvider.notifier).setActiveTenant(m);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(s.tenantSelected(m.tenant.companyName))),
|
||||
);
|
||||
},
|
||||
),
|
||||
]),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
|
||||
// Team management + Reports
|
||||
if (membership?.canManageUsers ?? false) ...[
|
||||
_SectionHeader(title: s.management),
|
||||
_InfoCard(children: [
|
||||
_NavTile(
|
||||
icon: Icons.group_outlined,
|
||||
iconColor: AppColors.inProgress,
|
||||
iconBg: AppColors.inProgressBg,
|
||||
title: s.team,
|
||||
subtitle: s.teamSub,
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const TenantTeamScreen()),
|
||||
),
|
||||
),
|
||||
_NavTile(
|
||||
icon: Icons.discount_outlined,
|
||||
iconColor: AppColors.success,
|
||||
iconBg: AppColors.successBg,
|
||||
title: s.discounts,
|
||||
subtitle: s.discountsSub,
|
||||
onTap: () => context.push(routeLabDiscounts),
|
||||
),
|
||||
_NavTile(
|
||||
icon: Icons.bar_chart_rounded,
|
||||
iconColor: AppColors.accent,
|
||||
iconBg: AppColors.inProgressBg,
|
||||
title: s.reports,
|
||||
subtitle: s.reportsSub,
|
||||
onTap: () => context.push(routeLabReports),
|
||||
),
|
||||
_NavTile(
|
||||
icon: Icons.auto_awesome_outlined,
|
||||
iconColor: const Color(0xFF7C3AED),
|
||||
iconBg: const Color(0xFFF3E8FF),
|
||||
title: s.aiAssistant,
|
||||
subtitle: s.aiAssistantSub,
|
||||
onTap: () => context.push(routeLabAi),
|
||||
),
|
||||
]),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
|
||||
// Preferences (language)
|
||||
_SectionHeader(title: s.preferences),
|
||||
_InfoCard(children: [
|
||||
_NavTile(
|
||||
icon: Icons.language_outlined,
|
||||
iconColor: AppColors.accent,
|
||||
iconBg: AppColors.inProgressBg,
|
||||
title: s.appLanguage,
|
||||
subtitle: _currentLanguageLabel(ref.watch(localeProvider).languageCode, s),
|
||||
onTap: () => _showLanguagePicker(context, ref, s),
|
||||
),
|
||||
]),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Sign out
|
||||
_SignOutCard(ref: ref, s: s),
|
||||
const SizedBox(height: 32),
|
||||
const Center(
|
||||
child: Text('DLS — Dental Lab System',
|
||||
style: TextStyle(fontSize: 12, color: AppColors.textMuted)),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showEditSheet(BuildContext context, WidgetRef ref, Tenant? tenant, AppStrings s) {
|
||||
if (tenant == null) return;
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (_) => _EditTenantSheet(
|
||||
tenant: tenant,
|
||||
s: s,
|
||||
onSave: (name, currency) async {
|
||||
await ref.read(authProvider.notifier).updateTenantInfo(
|
||||
tenantId: tenant.id,
|
||||
companyName: name,
|
||||
defaultCurrency: currency,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showLanguagePicker(BuildContext context, WidgetRef ref, AppStrings s) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (_) => _LanguagePickerSheet(s: s, ref: ref),
|
||||
);
|
||||
}
|
||||
|
||||
static String _tenantKindLabel(TenantKind? kind, AppStrings s) =>
|
||||
switch (kind) {
|
||||
TenantKind.clinic => s.tenantKindClinic,
|
||||
TenantKind.lab => s.tenantKindLab,
|
||||
null => '-',
|
||||
};
|
||||
|
||||
static String _currentLanguageLabel(String code, AppStrings s) => switch (code) {
|
||||
'en' => s.languageEnglish,
|
||||
'ru' => s.languageRussian,
|
||||
'ar' => s.languageArabic,
|
||||
'de' => s.languageGerman,
|
||||
_ => s.languageTurkish,
|
||||
};
|
||||
|
||||
static String _roleLabel(TenantRole? role, AppStrings s) => switch (role) {
|
||||
TenantRole.owner => s.roleOwner,
|
||||
TenantRole.admin => s.roleAdmin,
|
||||
TenantRole.technician => s.roleTechnician,
|
||||
TenantRole.delivery => s.roleDelivery,
|
||||
TenantRole.finance => s.roleFinance,
|
||||
TenantRole.doctor => s.roleDoctor,
|
||||
TenantRole.member => s.roleMember,
|
||||
null => '-',
|
||||
};
|
||||
}
|
||||
|
||||
// ── Language picker sheet ─────────────────────────────────────────────────────
|
||||
|
||||
class _LanguagePickerSheet extends ConsumerWidget {
|
||||
const _LanguagePickerSheet({required this.s, required this.ref});
|
||||
final AppStrings s;
|
||||
final WidgetRef ref;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef _) {
|
||||
final currentLocale = ref.watch(localeProvider);
|
||||
|
||||
final options = [
|
||||
('tr', '🇹🇷', s.languageTurkish),
|
||||
('en', '🇬🇧', s.languageEnglish),
|
||||
('ru', '🇷🇺', s.languageRussian),
|
||||
('ar', '🇸🇦', s.languageArabic),
|
||||
('de', '🇩🇪', s.languageGerman),
|
||||
];
|
||||
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Center(
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.border,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
s.languageSelection,
|
||||
style: const TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
for (final (code, flag, label) in options)
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
leading: Text(flag, style: const TextStyle(fontSize: 24)),
|
||||
title: Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
trailing: currentLocale.languageCode == code
|
||||
? const Icon(Icons.check_circle_rounded,
|
||||
color: AppColors.accent)
|
||||
: null,
|
||||
onTap: () {
|
||||
ref.read(localeProvider.notifier).setLocale(Locale(code));
|
||||
ref.read(authProvider.notifier).updateLanguage(code);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
SizedBox(height: MediaQuery.paddingOf(context).bottom + 4),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Edit sheet ────────────────────────────────────────────────────────────────
|
||||
|
||||
class _EditTenantSheet extends StatefulWidget {
|
||||
const _EditTenantSheet({
|
||||
required this.tenant,
|
||||
required this.s,
|
||||
required this.onSave,
|
||||
});
|
||||
final Tenant tenant;
|
||||
final AppStrings s;
|
||||
final Future<void> Function(String companyName, String currency) onSave;
|
||||
|
||||
@override
|
||||
State<_EditTenantSheet> createState() => _EditTenantSheetState();
|
||||
}
|
||||
|
||||
class _EditTenantSheetState extends State<_EditTenantSheet> {
|
||||
late final TextEditingController _nameController;
|
||||
late String _selectedCurrency;
|
||||
bool _saving = false;
|
||||
|
||||
static const _currencies = [
|
||||
('TRY', '₺', 'Türk Lirası'),
|
||||
('USD', '\$', 'US Dollar'),
|
||||
('EUR', '€', 'Euro'),
|
||||
('GBP', '£', 'British Pound'),
|
||||
('AED', 'د.إ', 'UAE Dirham'),
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_nameController = TextEditingController(text: widget.tenant.companyName);
|
||||
_selectedCurrency = widget.tenant.defaultCurrency;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
final name = _nameController.text.trim();
|
||||
if (name.isEmpty) return;
|
||||
setState(() => _saving = true);
|
||||
final navigator = Navigator.of(context);
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
try {
|
||||
await widget.onSave(name, _selectedCurrency);
|
||||
navigator.pop();
|
||||
} catch (e) {
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text('${widget.s.errorPrefix}: $e')));
|
||||
} finally {
|
||||
if (mounted) setState(() => _saving = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = widget.s;
|
||||
return Padding(
|
||||
padding:
|
||||
EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Center(
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.border,
|
||||
borderRadius: BorderRadius.circular(2)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(s.editLabInfo,
|
||||
style: const TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.textPrimary)),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _nameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: s.labName,
|
||||
hintText: s.labNameHint,
|
||||
),
|
||||
textCapitalization: TextCapitalization.words,
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Text(s.currency,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.textSecondary)),
|
||||
const SizedBox(height: 8),
|
||||
DropdownButtonFormField<String>(
|
||||
value: _selectedCurrency,
|
||||
decoration: InputDecoration(
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(color: AppColors.border)),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(color: AppColors.border)),
|
||||
),
|
||||
items: [
|
||||
for (final (code, symbol, name) in _currencies)
|
||||
DropdownMenuItem(
|
||||
value: code,
|
||||
child: Text('$symbol $name ($code)',
|
||||
style: const TextStyle(fontSize: 14)),
|
||||
),
|
||||
],
|
||||
onChanged: (v) {
|
||||
if (v != null) setState(() => _selectedCurrency = v);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
if (_saving)
|
||||
const Center(
|
||||
child: CircularProgressIndicator(color: AppColors.accent))
|
||||
else
|
||||
FilledButton(
|
||||
onPressed: _submit,
|
||||
style: FilledButton.styleFrom(
|
||||
minimumSize: const Size(double.infinity, 48)),
|
||||
child: Text(s.save),
|
||||
),
|
||||
SizedBox(height: MediaQuery.paddingOf(context).bottom + 4),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Reusable UI pieces ────────────────────────────────────────────────────────
|
||||
|
||||
class _UserCard extends StatelessWidget {
|
||||
const _UserCard({required this.profile});
|
||||
final dynamic profile;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final displayName = (profile?.displayName?.isNotEmpty == true)
|
||||
? profile!.displayName as String
|
||||
: 'Kullanıcı';
|
||||
final initial = (profile?.displayName?.isNotEmpty == true
|
||||
? (profile!.displayName as String)[0]
|
||||
: (profile?.email as String?)?[0] ?? '?')
|
||||
.toUpperCase();
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColors.border),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4))
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.inProgressBg,
|
||||
borderRadius: BorderRadius.circular(14)),
|
||||
child: Center(
|
||||
child: Text(initial,
|
||||
style: const TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.inProgress)),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(displayName,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textPrimary)),
|
||||
const SizedBox(height: 2),
|
||||
Text(profile?.email as String? ?? '',
|
||||
style: const TextStyle(
|
||||
fontSize: 13, color: AppColors.textSecondary)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SectionHeader extends StatelessWidget {
|
||||
const _SectionHeader({required this.title, this.action});
|
||||
final String title;
|
||||
final Widget? action;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.accent,
|
||||
letterSpacing: 0.3),
|
||||
),
|
||||
),
|
||||
if (action != null) action!,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _InfoCard extends StatelessWidget {
|
||||
const _InfoCard({required this.children});
|
||||
final List<Widget> children;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColors.border),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4))
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
for (int i = 0; i < children.length; i++) ...[
|
||||
children[i],
|
||||
if (i < children.length - 1)
|
||||
const Divider(
|
||||
height: 1, indent: 16, endIndent: 16, color: AppColors.border),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _InfoTile extends StatelessWidget {
|
||||
const _InfoTile(
|
||||
{required this.icon, required this.label, required this.value});
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final String value;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, size: 18, color: AppColors.textSecondary),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label,
|
||||
style: const TextStyle(
|
||||
fontSize: 11, color: AppColors.textMuted)),
|
||||
const SizedBox(height: 2),
|
||||
Text(value,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.textPrimary)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _InfoTileBadge extends StatelessWidget {
|
||||
const _InfoTileBadge({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.badgeColor,
|
||||
required this.badgeBg,
|
||||
});
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final String value;
|
||||
final Color badgeColor;
|
||||
final Color badgeBg;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, size: 18, color: AppColors.textSecondary),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(label,
|
||||
style: const TextStyle(
|
||||
fontSize: 11, color: AppColors.textMuted)),
|
||||
),
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: badgeBg,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(value,
|
||||
style: TextStyle(
|
||||
color: badgeColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NavTile extends StatelessWidget {
|
||||
const _NavTile({
|
||||
required this.icon,
|
||||
required this.iconColor,
|
||||
required this.iconBg,
|
||||
required this.title,
|
||||
required this.onTap,
|
||||
this.subtitle,
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final Color iconColor;
|
||||
final Color iconBg;
|
||||
final String title;
|
||||
final String? subtitle;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 2),
|
||||
leading: Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: iconBg, borderRadius: BorderRadius.circular(9)),
|
||||
child: Icon(icon, color: iconColor, size: 18),
|
||||
),
|
||||
title: Text(title,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600, color: AppColors.textPrimary)),
|
||||
subtitle: subtitle != null
|
||||
? Text(subtitle!,
|
||||
style: const TextStyle(color: AppColors.textSecondary))
|
||||
: null,
|
||||
trailing:
|
||||
const Icon(Icons.chevron_right, color: AppColors.textSecondary),
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SignOutCard extends StatelessWidget {
|
||||
const _SignOutCard({required this.ref, required this.s});
|
||||
final WidgetRef ref;
|
||||
final AppStrings s;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColors.cancelledBg),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4))
|
||||
],
|
||||
),
|
||||
child: ListTile(
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 2),
|
||||
leading: Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.cancelledBg,
|
||||
borderRadius: BorderRadius.circular(9)),
|
||||
child: const Icon(Icons.logout,
|
||||
color: AppColors.cancelled, size: 18),
|
||||
),
|
||||
title: Text(s.signOut,
|
||||
style: const TextStyle(
|
||||
color: AppColors.cancelled, fontWeight: FontWeight.w600)),
|
||||
onTap: () async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text(s.signOutTitle),
|
||||
content: Text(s.signOutConfirm),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: Text(s.cancel)),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: AppColors.cancelled),
|
||||
child: Text(s.signOut),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed == true) {
|
||||
await ref.read(authProvider.notifier).signOut();
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user