Files
lab-app/lib/features/lab/settings/lab_settings_screen.dart
T
Emre Emir 8bbc9dbff2 Initial commit: DLS - Dental Lab System
- Flutter + PocketBase dental lab management system
- Clinic & lab dashboards, job tracking, patient management
- Product catalog, finance tracking, multi-language support
- AI assistant integration, realtime notifications
- Windows installer (Inno Setup) included
- Developed by kovakyazilim.com
2026-06-11 15:57:31 +03:00

791 lines
25 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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: 4),
const Center(
child: Text('Geliştirici: kovakyazilim.com',
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();
}
},
),
);
}
}