Files
lab-app/lib/features/clinic/settings/clinic_settings_screen.dart
2026-06-20 18:24:40 +03:00

818 lines
27 KiB
Dart
Raw Permalink 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/location_completion_banner.dart';
import '../../shared/tenant_team_screen.dart';
import '../../shared/location_picker_sheet.dart';
import '../../shared/tenant_location_data.dart';
import '../connections/clinic_connections_screen.dart';
class ClinicSettingsScreen extends ConsumerWidget {
const ClinicSettingsScreen({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: [
if (tenant?.hasLocation != true) ...[
LocationCompletionBanner(
title: 'Konum eksik',
description:
'Kliniğiniz harita tabanlı aramalarda doğru eşleşme için koordinat bilgisine ihtiyaç duyuyor.',
buttonLabel: 'Konumu Düzenle',
onTap: () => _showEditSheet(context, ref, tenant, s),
compact: true,
),
const SizedBox(height: 20),
],
// User card
_SectionHeader(title: s.userInfo),
_UserCard(profile: profile),
const SizedBox(height: 20),
// Clinic info
_SectionHeader(
title: s.clinicInfo,
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.business,
label: s.clinicName,
value: tenant?.companyName ?? '-',
),
_InfoTile(
icon: Icons.category_outlined,
label: s.type,
value: _tenantKindLabel(tenant?.kind, s),
),
_InfoTile(
icon: Icons.star_outline,
label: s.role,
value: _roleLabel(membership?.role, s),
),
_InfoTile(
icon: Icons.place_outlined,
label: 'Konum',
value: tenant?.locationLabel.isNotEmpty == true
? tenant!.locationLabel
: '-',
),
]),
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.labConnections,
subtitle: s.labConnectionsSub,
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => const ClinicConnectionsScreen()),
),
),
]),
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.bar_chart_rounded,
iconColor: AppColors.accent,
iconBg: AppColors.inProgressBg,
title: s.reports,
subtitle: s.reportsSub,
onTap: () => context.push(routeClinicReports),
),
_NavTile(
icon: Icons.auto_awesome_outlined,
iconColor: const Color(0xFF7C3AED),
iconBg: const Color(0xFFF3E8FF),
title: s.aiAssistant,
subtitle: s.aiAssistantSub,
onTap: () => context.push(routeClinicAi),
),
_NavTile(
icon: Icons.workspace_premium_outlined,
iconColor: AppColors.primary,
iconBg: const Color(0xFFEFF6FF),
title: 'Paketler ve AI Kredileri',
subtitle: 'Trial ve paket görünümünü incele',
onTap: () => context.push(routeWelcome),
),
]),
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, location) async {
await ref.read(authProvider.notifier).updateTenantInfo(
tenantId: tenant.id,
companyName: name,
location: location,
);
},
),
);
}
void _showLanguagePicker(BuildContext context, WidgetRef ref, AppStrings s) {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (_) => _LanguagePickerSheet(s: s, ref: ref),
);
}
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 _tenantKindLabel(TenantKind? kind, AppStrings s) =>
switch (kind) {
TenantKind.clinic => s.tenantKindClinic,
TenantKind.lab => s.tenantKindLab,
null => '-',
};
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,
TenantLocationData location,
) onSave;
@override
State<_EditTenantSheet> createState() => _EditTenantSheetState();
}
class _EditTenantSheetState extends State<_EditTenantSheet> {
late final TextEditingController _nameController;
late final TextEditingController _addressController;
late final TextEditingController _cityController;
late final TextEditingController _districtController;
late TenantLocationData _location;
bool _saving = false;
@override
void initState() {
super.initState();
_nameController = TextEditingController(text: widget.tenant.companyName);
_location = TenantLocationData.fromTenant(widget.tenant);
_addressController = TextEditingController(text: _location.address ?? '');
_cityController = TextEditingController(text: _location.city ?? '');
_districtController = TextEditingController(text: _location.district ?? '');
}
@override
void dispose() {
_nameController.dispose();
_addressController.dispose();
_cityController.dispose();
_districtController.dispose();
super.dispose();
}
Future<void> _submit() async {
final name = _nameController.text.trim();
if (name.isEmpty) return;
final location = _location.copyWith(
address: _addressController.text.trim(),
city: _cityController.text.trim(),
district: _districtController.text.trim(),
);
if (!location.hasDetails) return;
setState(() => _saving = true);
final navigator = Navigator.of(context);
final messenger = ScaffoldMessenger.of(context);
try {
await widget.onSave(name, location);
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.editClinicInfo,
style: const TextStyle(
fontSize: 17,
fontWeight: FontWeight.w700,
color: AppColors.textPrimary)),
const SizedBox(height: 16),
TextFormField(
controller: _nameController,
decoration: InputDecoration(
labelText: s.clinicName,
hintText: s.clinicNameHint,
),
textCapitalization: TextCapitalization.words,
),
const SizedBox(height: 14),
Container(
width: double.infinity,
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.border),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Konum',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
),
),
const SizedBox(height: 8),
Text(
_location.fullLabel.isNotEmpty
? _location.fullLabel
: 'Henüz konum veya adres bilgisi girilmedi.',
style: const TextStyle(color: AppColors.textSecondary),
),
const SizedBox(height: 10),
OutlinedButton.icon(
onPressed: () async {
final picked = await showLocationPickerSheet(
context,
initialLocation: _location,
title: 'Klinik Konumu',
);
if (picked != null) {
setState(() => _location = picked);
}
},
icon: const Icon(Icons.map_outlined),
label: const Text('Haritadan Konum Seç'),
),
const SizedBox(height: 12),
TextFormField(
controller: _addressController,
decoration: const InputDecoration(
labelText: 'Açık Adres',
hintText: 'Cadde, sokak, mahalle bilgisi',
),
maxLines: 2,
textCapitalization: TextCapitalization.sentences,
),
const SizedBox(height: 10),
Row(
children: [
Expanded(
child: TextFormField(
controller: _cityController,
decoration: const InputDecoration(
labelText: 'Şehir',
),
textCapitalization: TextCapitalization.words,
),
),
const SizedBox(width: 10),
Expanded(
child: TextFormField(
controller: _districtController,
decoration: const InputDecoration(
labelText: 'İlçe',
),
textCapitalization: TextCapitalization.words,
),
),
],
),
],
),
),
const SizedBox(height: 20),
if (_saving)
const Center(
child: CircularProgressIndicator(color: AppColors.accent))
else
FilledButton(
onPressed: _saving ? null : _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 _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();
}
},
),
);
}
}