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
This commit is contained in:
Emre Emir
2026-06-11 15:57:31 +03:00
commit 8bbc9dbff2
226 changed files with 31308 additions and 0 deletions
@@ -0,0 +1,790 @@
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();
}
},
),
);
}
}