Add pricing entry flow and platform admin foundations
This commit is contained in:
@@ -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',
|
||||
});
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user