Files
lab-app/lib/features/auth/onboarding_screen.dart
2026-06-20 18:24:40 +03:00

535 lines
22 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 '../../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 {
const OnboardingScreen({super.key});
@override
ConsumerState<OnboardingScreen> createState() => _OnboardingScreenState();
}
class _OnboardingScreenState extends ConsumerState<OnboardingScreen>
with SingleTickerProviderStateMixin {
final _formKey = GlobalKey<FormState>();
final _nameCtrl = TextEditingController();
String _selectedKind = 'clinic';
TenantLocationData? _location;
bool _loading = false;
String? _error;
late AnimationController _animCtrl;
late Animation<double> _fadeAnim;
late Animation<Offset> _slideAnim;
@override
void initState() {
super.initState();
_animCtrl = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 600),
);
_fadeAnim = CurvedAnimation(parent: _animCtrl, curve: Curves.easeOut);
_slideAnim = Tween<Offset>(
begin: const Offset(0, 0.08),
end: Offset.zero,
).animate(CurvedAnimation(parent: _animCtrl, curve: Curves.easeOutCubic));
_animCtrl.forward();
}
@override
void dispose() {
_animCtrl.dispose();
_nameCtrl.dispose();
super.dispose();
}
Future<void> _create() async {
if (!_formKey.currentState!.validate()) return;
setState(() {
_loading = true;
_error = null;
});
try {
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);
} catch (e) {
setState(() {
_error = 'Hesap oluşturulamadı. Lütfen tekrar deneyin.';
_loading = false;
});
}
}
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final size = MediaQuery.sizeOf(context);
return Scaffold(
backgroundColor: const Color(0xFF4F46E5),
body: Stack(
children: [
// ── Gradient background ──────────────────────────────────────────
Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomCenter,
colors: [Color(0xFF3730A3), Color(0xFF6366F1)],
),
),
),
// ── Decorative circles ───────────────────────────────────────────
Positioned(
top: -40,
right: -60,
child: Container(
width: 220,
height: 220,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white.withValues(alpha: 0.06),
),
),
),
Positioned(
top: 80,
left: -70,
child: Container(
width: 200,
height: 200,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white.withValues(alpha: 0.04),
),
),
),
// ── Content ──────────────────────────────────────────────────────
Column(
children: [
// Header
SafeArea(
bottom: false,
child: SizedBox(
height: size.height * 0.26,
child: Stack(
children: [
// Sign out
Positioned(
right: 8,
top: 4,
child: TextButton.icon(
onPressed: () =>
ref.read(authProvider.notifier).signOut(),
icon: const Icon(Icons.logout_rounded,
color: Colors.white70, size: 18),
label: const Text(
'Çıkış',
style: TextStyle(color: Colors.white70),
),
),
),
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 68,
height: 68,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: Colors.white.withValues(alpha: 0.3),
width: 1.5,
),
),
child: const Icon(
Icons.domain_add_rounded,
size: 32,
color: Colors.white,
),
),
const SizedBox(height: 14),
const Text(
'Kurumunuzu Oluşturun',
style: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.w700,
letterSpacing: 0.3,
),
),
const SizedBox(height: 4),
Text(
'Klinik veya laboratuvar olarak kayıt olun',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.70),
fontSize: 13,
),
),
],
),
),
],
),
),
),
// Form card
Expanded(
child: FadeTransition(
opacity: _fadeAnim,
child: SlideTransition(
position: _slideAnim,
child: Container(
decoration: BoxDecoration(
color: cs.surface,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(32),
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.15),
blurRadius: 24,
offset: const Offset(0, -4),
),
],
),
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(28, 32, 28, 24),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'Kurum Türünü Seçin',
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(fontWeight: FontWeight.w600),
),
const SizedBox(height: 14),
// Kind cards
Row(
children: [
Expanded(
child: _KindCard(
icon: Icons.local_hospital_outlined,
label: 'Klinik',
description: 'Diş kliniği',
value: 'clinic',
selected: _selectedKind == 'clinic',
onTap: () => setState(
() => _selectedKind = 'clinic'),
),
),
const SizedBox(width: 12),
Expanded(
child: _KindCard(
icon: Icons.science_outlined,
label: 'Laboratuvar',
description: 'Diş laboratuvarı',
value: 'lab',
selected: _selectedKind == 'lab',
onTap: () =>
setState(() => _selectedKind = 'lab'),
),
),
],
),
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ı',
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(fontWeight: FontWeight.w600),
),
const SizedBox(height: 10),
TextFormField(
controller: _nameCtrl,
textInputAction: TextInputAction.done,
textCapitalization: TextCapitalization.words,
onFieldSubmitted: (_) => _create(),
decoration: InputDecoration(
labelText: _selectedKind == 'clinic'
? 'Klinik Adı'
: 'Laboratuvar Adı',
prefixIcon: const Icon(
Icons.business_outlined,
size: 20),
filled: true,
fillColor: cs.surfaceContainerHighest,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(
color: Color(0xFF4F46E5), width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide:
BorderSide(color: cs.error, width: 1.5),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide:
BorderSide(color: cs.error, width: 2),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 16),
),
validator: (v) {
if (v == null || v.trim().isEmpty) {
return 'Kurum adı gereklidir';
}
if (v.trim().length < 3) {
return 'En az 3 karakter olmalıdır';
}
return null;
},
),
// Error banner
if (_error != null) ...[
const SizedBox(height: 14),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: cs.errorContainer,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Icon(Icons.error_outline_rounded,
color: cs.onErrorContainer, size: 18),
const SizedBox(width: 8),
Expanded(
child: Text(
_error!,
style: TextStyle(
color: cs.onErrorContainer,
fontSize: 13),
),
),
],
),
),
],
const SizedBox(height: 28),
FilledButton(
onPressed: _loading || _location == null
? null
: _create,
style: FilledButton.styleFrom(
minimumSize: const Size.fromHeight(52),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
backgroundColor: const Color(0xFF4F46E5),
),
child: _loading
? const SizedBox(
height: 22,
width: 22,
child: CircularProgressIndicator(
strokeWidth: 2.5,
color: Colors.white,
),
)
: const Text(
'Devam Et',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
),
),
),
),
),
],
),
],
),
);
}
}
class _KindCard extends StatelessWidget {
const _KindCard({
required this.icon,
required this.label,
required this.description,
required this.value,
required this.selected,
required this.onTap,
});
final IconData icon;
final String label;
final String description;
final String value;
final bool selected;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(16),
child: AnimatedContainer(
duration: const Duration(milliseconds: 180),
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 12),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: selected ? const Color(0xFF4F46E5) : cs.outlineVariant,
width: selected ? 2 : 1,
),
color: selected
? const Color(0xFF4F46E5).withValues(alpha: 0.08)
: cs.surfaceContainerLow,
),
child: Column(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: selected
? const Color(0xFF4F46E5).withValues(alpha: 0.12)
: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
),
child: Icon(
icon,
size: 26,
color: selected ? const Color(0xFF4F46E5) : cs.onSurfaceVariant,
),
),
const SizedBox(height: 10),
Text(
label,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14,
color: selected ? const Color(0xFF4F46E5) : cs.onSurface,
),
),
const SizedBox(height: 2),
Text(
description,
style: TextStyle(
fontSize: 11,
color: cs.onSurfaceVariant,
),
),
],
),
),
);
}
}