Add pricing entry flow and platform admin foundations

This commit is contained in:
egecankomur
2026-06-20 18:24:40 +03:00
parent 1d36ccdf30
commit ac42681f7e
44 changed files with 6567 additions and 1419 deletions
@@ -9,7 +9,10 @@ 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 {
@@ -29,6 +32,17 @@ class ClinicSettingsScreen extends ConsumerWidget {
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),
@@ -62,6 +76,13 @@ class ClinicSettingsScreen extends ConsumerWidget {
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),
@@ -100,7 +121,9 @@ class ClinicSettingsScreen extends ConsumerWidget {
onTap: () {
ref.read(authProvider.notifier).setActiveTenant(m);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(s.tenantSelected(m.tenant.companyName))),
SnackBar(
content:
Text(s.tenantSelected(m.tenant.companyName))),
);
},
),
@@ -120,8 +143,7 @@ class ClinicSettingsScreen extends ConsumerWidget {
subtitle: s.teamSub,
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => const TenantTeamScreen()),
MaterialPageRoute(builder: (_) => const TenantTeamScreen()),
),
),
_NavTile(
@@ -140,6 +162,14 @@ class ClinicSettingsScreen extends ConsumerWidget {
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),
],
@@ -152,7 +182,8 @@ class ClinicSettingsScreen extends ConsumerWidget {
iconColor: AppColors.accent,
iconBg: AppColors.inProgressBg,
title: s.appLanguage,
subtitle: _currentLanguageLabel(ref.watch(localeProvider).languageCode, s),
subtitle: _currentLanguageLabel(
ref.watch(localeProvider).languageCode, s),
onTap: () => _showLanguagePicker(context, ref, s),
),
]),
@@ -176,7 +207,8 @@ class ClinicSettingsScreen extends ConsumerWidget {
);
}
void _showEditSheet(BuildContext context, WidgetRef ref, Tenant? tenant, AppStrings s) {
void _showEditSheet(
BuildContext context, WidgetRef ref, Tenant? tenant, AppStrings s) {
if (tenant == null) return;
showModalBottomSheet(
context: context,
@@ -185,10 +217,12 @@ class ClinicSettingsScreen extends ConsumerWidget {
builder: (_) => _EditTenantSheet(
tenant: tenant,
s: s,
onSave: (name) async {
await ref
.read(authProvider.notifier)
.updateTenantInfo(tenantId: tenant.id, companyName: name);
onSave: (name, location) async {
await ref.read(authProvider.notifier).updateTenantInfo(
tenantId: tenant.id,
companyName: name,
location: location,
);
},
),
);
@@ -202,7 +236,8 @@ class ClinicSettingsScreen extends ConsumerWidget {
);
}
static String _currentLanguageLabel(String code, AppStrings s) => switch (code) {
static String _currentLanguageLabel(String code, AppStrings s) =>
switch (code) {
'en' => s.languageEnglish,
'ru' => s.languageRussian,
'ar' => s.languageArabic,
@@ -316,7 +351,10 @@ class _EditTenantSheet extends StatefulWidget {
});
final Tenant tenant;
final AppStrings s;
final Future<void> Function(String companyName) onSave;
final Future<void> Function(
String companyName,
TenantLocationData location,
) onSave;
@override
State<_EditTenantSheet> createState() => _EditTenantSheetState();
@@ -324,32 +362,49 @@ class _EditTenantSheet extends StatefulWidget {
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);
await widget.onSave(name, location);
navigator.pop();
} catch (e) {
messenger.showSnackBar(
SnackBar(content: Text('${widget.s.errorPrefix}: $e')));
messenger
.showSnackBar(SnackBar(content: Text('${widget.s.errorPrefix}: $e')));
} finally {
if (mounted) setState(() => _saving = false);
}
@@ -395,13 +450,91 @@ class _EditTenantSheetState extends State<_EditTenantSheet> {
),
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: _submit,
onPressed: _saving ? null : _submit,
style: FilledButton.styleFrom(
minimumSize: const Size(double.infinity, 48)),
child: Text(s.save),
@@ -534,7 +667,10 @@ class _InfoCard extends StatelessWidget {
children[i],
if (i < children.length - 1)
const Divider(
height: 1, indent: 16, endIndent: 16, color: AppColors.border),
height: 1,
indent: 16,
endIndent: 16,
color: AppColors.border),
],
],
),
@@ -599,8 +735,7 @@ class _NavTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListTile(
contentPadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 2),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 2),
leading: Container(
width: 36,
height: 36,
@@ -615,8 +750,7 @@ class _NavTile extends StatelessWidget {
? Text(subtitle!,
style: const TextStyle(color: AppColors.textSecondary))
: null,
trailing:
const Icon(Icons.chevron_right, color: AppColors.textSecondary),
trailing: const Icon(Icons.chevron_right, color: AppColors.textSecondary),
onTap: onTap,
);
}
@@ -642,16 +776,14 @@ class _SignOutCard extends StatelessWidget {
],
),
child: ListTile(
contentPadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 2),
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),
child: const Icon(Icons.logout, color: AppColors.cancelled, size: 18),
),
title: Text(s.signOut,
style: const TextStyle(