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
@@ -11,12 +11,22 @@ class OnboardingRepository {
Future<AuthResult> createTenantAndJoin({
required String kind,
required String companyName,
String? companyAddress,
String? city,
String? district,
double? latitude,
double? longitude,
}) async {
final userId = _pb.authStore.record!.id;
final tenant = await _pb.collection('tenants').create(body: {
'kind': kind,
'company_name': companyName,
'company_address': companyAddress,
'city': city,
'district': district,
'latitude': latitude,
'longitude': longitude,
'status': 'active',
'default_currency': 'TRY',
});
+79 -6
View File
@@ -1,6 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/providers/auth_provider.dart';
import '../../core/theme/app_theme.dart';
import '../shared/location_picker_sheet.dart';
import '../shared/tenant_location_data.dart';
import 'onboarding_repository.dart';
class OnboardingScreen extends ConsumerStatefulWidget {
@@ -15,6 +18,7 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen>
final _formKey = GlobalKey<FormState>();
final _nameCtrl = TextEditingController();
String _selectedKind = 'clinic';
TenantLocationData? _location;
bool _loading = false;
String? _error;
late AnimationController _animCtrl;
@@ -53,6 +57,11 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen>
final result = await OnboardingRepository.instance.createTenantAndJoin(
kind: _selectedKind,
companyName: _nameCtrl.text.trim(),
companyAddress: _location?.address,
city: _location?.city,
district: _location?.district,
latitude: _location?.latitude,
longitude: _location?.longitude,
);
if (!mounted) return;
ref.read(authProvider.notifier).setActiveTenant(result.tenants.first);
@@ -249,6 +258,70 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen>
const SizedBox(height: 24),
Text(
'Konum',
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(fontWeight: FontWeight.w600),
),
const SizedBox(height: 10),
Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(14),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_location?.fullLabel.isNotEmpty == true
? _location!.fullLabel
: 'Konumu haritadan seçin. Laboratuvar aramalarında bu veri kullanılacak.',
style: TextStyle(
fontSize: 13,
color: _location == null
? cs.onSurfaceVariant
: cs.onSurface,
),
),
if (_location?.hasCoordinates == true) ...[
const SizedBox(height: 8),
Text(
'${_location!.latitude!.toStringAsFixed(6)}, ${_location!.longitude!.toStringAsFixed(6)}',
style: const TextStyle(
fontSize: 12,
color: AppColors.textMuted,
),
),
],
const SizedBox(height: 12),
OutlinedButton.icon(
onPressed: () async {
final picked =
await showLocationPickerSheet(
context,
initialLocation: _location,
title: _selectedKind == 'lab'
? 'Laboratuvar Konumu'
: 'Klinik Konumu',
);
if (picked != null) {
setState(() => _location = picked);
}
},
icon: const Icon(Icons.map_outlined),
label: Text(_location == null
? 'Haritadan Seç'
: 'Konumu Güncelle'),
),
],
),
),
const SizedBox(height: 24),
// Company name
Text(
'Kurum Adı',
@@ -287,8 +360,8 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen>
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide(
color: cs.error, width: 1.5),
borderSide:
BorderSide(color: cs.error, width: 1.5),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
@@ -340,7 +413,9 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen>
const SizedBox(height: 28),
FilledButton(
onPressed: _loading ? null : _create,
onPressed: _loading || _location == null
? null
: _create,
style: FilledButton.styleFrom(
minimumSize: const Size.fromHeight(52),
shape: RoundedRectangleBorder(
@@ -431,9 +506,7 @@ class _KindCard extends StatelessWidget {
child: Icon(
icon,
size: 26,
color: selected
? const Color(0xFF4F46E5)
: cs.onSurfaceVariant,
color: selected ? const Color(0xFF4F46E5) : cs.onSurfaceVariant,
),
),
const SizedBox(height: 10),
+70 -64
View File
@@ -40,9 +40,7 @@ class _SignInScreenState extends ConsumerState<SignInScreen> {
Future<void> _submit() async {
if (!_formKey.currentState!.validate()) return;
await ref
.read(authProvider.notifier)
.signIn(
await ref.read(authProvider.notifier).signIn(
_emailCtrl.text.trim(),
_passCtrl.text,
rememberSession: _rememberMe,
@@ -98,10 +96,13 @@ class _SignInScreenState extends ConsumerState<SignInScreen> {
),
],
),
child:
const Center(child: ToothLogo(size: 34, color: Colors.white)),
child: const Center(
child: ToothLogo(size: 34, color: Colors.white)),
),
).animate().fadeIn(duration: 400.ms).scale(begin: const Offset(0.8, 0.8)),
)
.animate()
.fadeIn(duration: 400.ms)
.scale(begin: const Offset(0.8, 0.8)),
const SizedBox(height: 24),
Center(
@@ -114,7 +115,10 @@ class _SignInScreenState extends ConsumerState<SignInScreen> {
letterSpacing: -0.5,
),
),
).animate(delay: 60.ms).fadeIn(duration: 400.ms).slideY(begin: 0.1),
)
.animate(delay: 60.ms)
.fadeIn(duration: 400.ms)
.slideY(begin: 0.1),
const SizedBox(height: 6),
Center(
child: Text(
@@ -169,10 +173,18 @@ class _SignInScreenState extends ConsumerState<SignInScreen> {
),
),
),
const Positioned(top: -140, left: -140, child: _Ring(size: 520, opacity: 0.06)),
const Positioned(bottom: -100, right: -100, child: _Ring(size: 400, opacity: 0.05)),
const Positioned(top: 160, right: 60, child: _Ring(size: 100, opacity: 0.09)),
const Positioned(bottom: 220, left: 60, child: _Ring(size: 70, opacity: 0.07)),
const Positioned(
top: -140,
left: -140,
child: _Ring(size: 520, opacity: 0.06)),
const Positioned(
bottom: -100,
right: -100,
child: _Ring(size: 400, opacity: 0.05)),
const Positioned(
top: 160, right: 60, child: _Ring(size: 100, opacity: 0.09)),
const Positioned(
bottom: 220, left: 60, child: _Ring(size: 70, opacity: 0.07)),
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 64, vertical: 52),
@@ -356,7 +368,6 @@ class _SignInScreenState extends ConsumerState<SignInScreen> {
(v == null || v.trim().isEmpty) ? s.emailRequired : null,
),
const SizedBox(height: 14),
_Field(
controller: _passCtrl,
label: s.password,
@@ -377,44 +388,29 @@ class _SignInScreenState extends ConsumerState<SignInScreen> {
validator: (v) =>
(v == null || v.isEmpty) ? s.passwordRequired : null,
),
const SizedBox(height: 12),
InkWell(
borderRadius: BorderRadius.circular(10),
onTap: auth.isLoading
CheckboxListTile(
value: _rememberMe,
onChanged: auth.isLoading
? null
: () => setState(() => _rememberMe = !_rememberMe),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
Checkbox(
value: _rememberMe,
onChanged: auth.isLoading
? null
: (value) => setState(() => _rememberMe = value ?? true),
activeColor: const Color(0xFF0D4C85),
),
const SizedBox(width: 6),
Text(
s.rememberMe,
style: const TextStyle(
fontSize: 14,
color: AppColors.textSecondary,
fontWeight: FontWeight.w500,
),
),
],
: (value) => setState(() => _rememberMe = value ?? true),
activeColor: const Color(0xFF0D4C85),
controlAffinity: ListTileControlAffinity.leading,
contentPadding: EdgeInsets.zero,
dense: true,
title: Text(
s.rememberMe,
style: const TextStyle(
fontSize: 14,
color: AppColors.textSecondary,
fontWeight: FontWeight.w500,
),
),
),
if (auth.error != null) ...[
const SizedBox(height: 14),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: const Color(0xFFFEF2F2),
borderRadius: BorderRadius.circular(10),
@@ -437,9 +433,7 @@ class _SignInScreenState extends ConsumerState<SignInScreen> {
),
),
],
const SizedBox(height: 24),
DecoratedBox(
decoration: BoxDecoration(
gradient: const LinearGradient(
@@ -488,24 +482,36 @@ class _SignInScreenState extends ConsumerState<SignInScreen> {
// ── Sign-up link ───────────────────────────────────────────────────────────
Widget _buildSignUpLink(BuildContext context, AppStrings s) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
return Column(
children: [
Text(
s.noAccount,
style:
const TextStyle(color: AppColors.textSecondary, fontSize: 14),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
s.noAccount,
style:
const TextStyle(color: AppColors.textSecondary, fontSize: 14),
),
TextButton(
onPressed: () => context.go(routeSignUp),
style: TextButton.styleFrom(
foregroundColor: const Color(0xFF0D4C85),
padding: const EdgeInsets.symmetric(horizontal: 8),
),
child: Text(
s.signUp,
style:
const TextStyle(fontWeight: FontWeight.w700, fontSize: 14),
),
),
],
),
TextButton(
onPressed: () => context.go(routeSignUp),
TextButton.icon(
onPressed: () => context.go(routeWelcome),
icon: const Icon(Icons.workspace_premium_outlined, size: 18),
label: const Text('Paketleri İncele'),
style: TextButton.styleFrom(
foregroundColor: const Color(0xFF0D4C85),
padding: const EdgeInsets.symmetric(horizontal: 8),
),
child: Text(
s.signUp,
style:
const TextStyle(fontWeight: FontWeight.w700, fontSize: 14),
foregroundColor: AppColors.textSecondary,
),
),
],
@@ -726,7 +732,8 @@ class _DashboardPreviewCard extends StatelessWidget {
const SizedBox(height: 18),
const Row(
children: [
_StatChip(value: '24', label: 'Aktif', color: Color(0xFF60A5FA)),
_StatChip(
value: '24', label: 'Aktif', color: Color(0xFF60A5FA)),
SizedBox(width: 8),
_StatChip(
value: '8', label: 'Bekliyor', color: Color(0xFFFBBF24)),
@@ -915,8 +922,7 @@ class _Field extends StatelessWidget {
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide:
const BorderSide(color: AppColors.cancelled, width: 1.5),
borderSide: const BorderSide(color: AppColors.cancelled, width: 1.5),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
@@ -924,8 +930,8 @@ class _Field extends StatelessWidget {
),
contentPadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
labelStyle: const TextStyle(
color: AppColors.textSecondary, fontSize: 14),
labelStyle:
const TextStyle(color: AppColors.textSecondary, fontSize: 14),
),
);
}
File diff suppressed because it is too large Load Diff