Files
Emre Emir 8bbc9dbff2 Initial commit: DLS - Dental Lab System
- Flutter + PocketBase dental lab management system
- Clinic & lab dashboards, job tracking, patient management
- Product catalog, finance tracking, multi-language support
- AI assistant integration, realtime notifications
- Windows installer (Inno Setup) included
- Developed by kovakyazilim.com
2026-06-11 15:57:31 +03:00

620 lines
23 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_animate/flutter_animate.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../core/providers/auth_provider.dart';
import '../../core/router/app_router.dart';
import '../../core/theme/app_theme.dart';
import '../../core/widgets/tooth_logo.dart';
import 'auth_widgets.dart';
class SignUpScreen extends ConsumerStatefulWidget {
const SignUpScreen({super.key});
@override
ConsumerState<SignUpScreen> createState() => _SignUpScreenState();
}
class _SignUpScreenState extends ConsumerState<SignUpScreen> {
final _formKey = GlobalKey<FormState>();
final _firstNameCtrl = TextEditingController();
final _lastNameCtrl = TextEditingController();
final _emailCtrl = TextEditingController();
final _passCtrl = TextEditingController();
final _confirmPassCtrl = TextEditingController();
bool _obscure = true;
bool _obscureConfirm = true;
bool _loading = false;
String? _error;
@override
void dispose() {
_firstNameCtrl.dispose();
_lastNameCtrl.dispose();
_emailCtrl.dispose();
_passCtrl.dispose();
_confirmPassCtrl.dispose();
super.dispose();
}
Future<void> _submit() async {
if (!_formKey.currentState!.validate()) return;
setState(() {
_loading = true;
_error = null;
});
try {
await ref.read(authProvider.notifier).register(
email: _emailCtrl.text.trim(),
password: _passCtrl.text,
firstName: _firstNameCtrl.text.trim(),
lastName: _lastNameCtrl.text.trim(),
);
} catch (e) {
setState(() {
_error = _parseError(e.toString());
_loading = false;
});
}
}
String _parseError(String msg) {
if (msg.contains('already') || msg.contains('unique') || msg.contains('UNIQUE')) {
return 'Bu e-posta adresi zaten kayıtlı.';
}
if (msg.contains('403') || msg.contains('Forbidden')) {
return 'Kayıt şu anda kapalı. Lütfen yönetici ile iletişime geçin.';
}
return 'Kayıt olunamadı. Lütfen tekrar deneyin.';
}
@override
Widget build(BuildContext context) {
final isDesktop = MediaQuery.sizeOf(context).width > 800;
return Scaffold(
backgroundColor: AppColors.background,
body: isDesktop ? _buildDesktop(context) : _buildMobile(context),
);
}
// ── Mobile layout ──────────────────────────────────────────────────────────
Widget _buildMobile(BuildContext context) {
return SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: _buildForm(context, isMobile: true),
),
);
}
// ── Desktop layout ─────────────────────────────────────────────────────────
Widget _buildDesktop(BuildContext context) {
return Row(
children: [
// LEFT PANEL — solid gradient + white animated blobs on top
Expanded(
flex: 5,
child: Stack(
fit: StackFit.expand,
children: [
Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [AppColors.primary, Color(0xFF1A5C8A)],
),
),
),
const AnimatedAuthBg(bright: true),
Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 56),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 72,
height: 72,
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 Center(child: ToothLogo(size: 38, color: Colors.white)),
),
const SizedBox(height: 24),
const Text(
'DLS',
style: TextStyle(
fontSize: 48,
fontWeight: FontWeight.w800,
color: Colors.white,
letterSpacing: 2,
),
),
const SizedBox(height: 4),
Text(
'Dental Lab Sistemi',
style: TextStyle(
fontSize: 17,
color: Colors.white.withValues(alpha: 0.7),
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 48),
const _FeatureBullet(
icon: Icons.dashboard_rounded,
text: 'İş takibi tek ekranda',
),
const SizedBox(height: 16),
const _FeatureBullet(
icon: Icons.link_rounded,
text: 'Klinik-lab bağlantısı',
),
const SizedBox(height: 16),
const _FeatureBullet(
icon: Icons.bolt_rounded,
text: 'Gerçek zamanlı durum',
),
],
),
),
),
],
),
),
// RIGHT PANEL — light gray so white card stands out
Container(
width: 480,
color: AppColors.background,
child: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) => SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: constraints.maxHeight),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [_buildForm(context, isMobile: false)],
),
),
),
),
),
),
),
),
],
);
}
// ── Shared form content ────────────────────────────────────────────────────
Widget _buildForm(BuildContext context, {required bool isMobile}) {
return Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (isMobile) const SizedBox(height: 48),
// ── Back button + branding (mobile only) ───────────────────────
if (isMobile) ...[
Row(
children: [
IconButton(
onPressed: () => context.go(routeSignIn),
icon: const Icon(Icons.arrow_back_ios_new_rounded, size: 20),
style: IconButton.styleFrom(
foregroundColor: AppColors.textPrimary,
backgroundColor: AppColors.surface,
padding: const EdgeInsets.all(10),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: const BorderSide(color: AppColors.border),
),
),
),
],
).animate().fadeIn(duration: 300.ms),
const SizedBox(height: 28),
Center(
child: Column(
children: [
Container(
width: 72,
height: 72,
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [AppColors.primary, AppColors.accent],
),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: AppColors.accent.withValues(alpha: 0.3),
blurRadius: 18,
offset: const Offset(0, 6),
),
],
),
child: const Icon(
Icons.person_add_alt_1_rounded,
size: 32,
color: Colors.white,
),
),
const SizedBox(height: 16),
const Text(
'Hesap Oluştur',
style: TextStyle(
fontSize: 26,
fontWeight: FontWeight.w800,
color: AppColors.textPrimary,
),
),
const SizedBox(height: 4),
const Text(
'DLS ağına katılın',
style: TextStyle(
fontSize: 14,
color: AppColors.textSecondary,
fontWeight: FontWeight.w500,
),
),
],
),
).animate().fadeIn(duration: 400.ms).slideY(begin: -0.08, end: 0),
const SizedBox(height: 32),
],
// Desktop back button (outside card)
if (!isMobile) ...[
Row(
children: [
IconButton(
onPressed: () => context.go(routeSignIn),
icon: const Icon(Icons.arrow_back_ios_new_rounded, size: 18),
style: IconButton.styleFrom(
foregroundColor: AppColors.textPrimary,
backgroundColor: AppColors.surface,
padding: const EdgeInsets.all(8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
side: const BorderSide(color: AppColors.border),
),
),
),
],
).animate().fadeIn(duration: 300.ms),
const SizedBox(height: 20),
],
// ── Form card ──────────────────────────────────────────────────
Container(
padding: EdgeInsets.all(isMobile ? 24 : 32),
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: AppColors.border),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: isMobile ? 0.05 : 0.09),
blurRadius: isMobile ? 16 : 28,
spreadRadius: 0,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Heading inside card on desktop
if (!isMobile) ...[
const Text(
'Hesap Oluştur',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w800,
color: AppColors.textPrimary,
),
).animate().fadeIn(duration: 400.ms).slideY(begin: -0.08, end: 0),
const SizedBox(height: 4),
const Text(
'DLS ağına katılın',
style: TextStyle(fontSize: 14, color: AppColors.textSecondary),
).animate(delay: 40.ms).fadeIn(duration: 400.ms),
const SizedBox(height: 24),
],
// Ad / Soyad satırı
Row(
children: [
Expanded(
child: _Field(
controller: _firstNameCtrl,
label: 'Ad',
icon: Icons.badge_outlined,
textCapitalization: TextCapitalization.words,
textInputAction: TextInputAction.next,
validator: (v) =>
(v == null || v.trim().isEmpty) ? 'Gerekli' : null,
),
),
const SizedBox(width: 12),
Expanded(
child: _Field(
controller: _lastNameCtrl,
label: 'Soyad',
icon: Icons.badge_outlined,
textCapitalization: TextCapitalization.words,
textInputAction: TextInputAction.next,
validator: (v) =>
(v == null || v.trim().isEmpty) ? 'Gerekli' : null,
),
),
],
),
const SizedBox(height: 12),
_Field(
controller: _emailCtrl,
label: 'E-posta',
icon: Icons.email_outlined,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
validator: (v) {
if (v == null || v.trim().isEmpty) return 'E-posta gereklidir';
if (!v.contains('@')) return 'Geçerli bir e-posta girin';
return null;
},
),
const SizedBox(height: 12),
_Field(
controller: _passCtrl,
label: 'Şifre',
icon: Icons.lock_outline_rounded,
obscureText: _obscure,
textInputAction: TextInputAction.next,
suffixIcon: IconButton(
icon: Icon(
_obscure ? Icons.visibility_outlined : Icons.visibility_off_outlined,
size: 20,
color: AppColors.textSecondary,
),
onPressed: () => setState(() => _obscure = !_obscure),
),
validator: (v) {
if (v == null || v.isEmpty) return 'Şifre gereklidir';
if (v.length < 8) return 'En az 8 karakter olmalıdır';
return null;
},
),
const SizedBox(height: 12),
_Field(
controller: _confirmPassCtrl,
label: 'Şifre Tekrar',
icon: Icons.lock_outline_rounded,
obscureText: _obscureConfirm,
textInputAction: TextInputAction.done,
onFieldSubmitted: (_) => _submit(),
suffixIcon: IconButton(
icon: Icon(
_obscureConfirm
? Icons.visibility_outlined
: Icons.visibility_off_outlined,
size: 20,
color: AppColors.textSecondary,
),
onPressed: () => setState(() => _obscureConfirm = !_obscureConfirm),
),
validator: (v) =>
(v != _passCtrl.text) ? 'Şifreler eşleşmiyor' : null,
),
if (_error != null) ...[
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: AppColors.cancelledBg,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: AppColors.cancelled.withValues(alpha: 0.3)),
),
child: Row(
children: [
const Icon(Icons.error_outline_rounded,
color: AppColors.cancelled, size: 16),
const SizedBox(width: 8),
Expanded(
child: Text(
_error!,
style: const TextStyle(
color: AppColors.cancelled, fontSize: 13),
),
),
],
),
),
],
const SizedBox(height: 20),
FilledButton(
onPressed: _loading ? null : _submit,
style: FilledButton.styleFrom(
backgroundColor: AppColors.primary,
minimumSize: const Size.fromHeight(52),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
),
child: _loading
? const SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(
strokeWidth: 2.5, color: Colors.white),
)
: const Text(
'Kayıt Ol',
style:
TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
),
),
],
),
).animate(delay: 100.ms).fadeIn(duration: 400.ms).slideY(begin: 0.1, end: 0),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Zaten hesabın var mı?',
style: TextStyle(color: AppColors.textSecondary, fontSize: 14),
),
TextButton(
onPressed: () => context.go(routeSignIn),
style: TextButton.styleFrom(
foregroundColor: AppColors.accent,
padding: const EdgeInsets.symmetric(horizontal: 8),
),
child: const Text(
'Giriş Yap',
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14),
),
),
],
).animate(delay: 200.ms).fadeIn(duration: 400.ms),
SizedBox(height: isMobile ? 32 : 16),
],
),
);
}
}
// ── Feature bullet (desktop left panel) ──────────────────────────────────────
class _FeatureBullet extends StatelessWidget {
const _FeatureBullet({required this.icon, required this.text});
final IconData icon;
final String text;
@override
Widget build(BuildContext context) {
return Row(
children: [
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(10),
),
child: Icon(icon, size: 18, color: Colors.white),
),
const SizedBox(width: 14),
Text(
text,
style: TextStyle(
fontSize: 15,
color: Colors.white.withValues(alpha: 0.9),
fontWeight: FontWeight.w500,
),
),
],
);
}
}
// ── Form field ────────────────────────────────────────────────────────────────
class _Field extends StatelessWidget {
const _Field({
required this.controller,
required this.label,
required this.icon,
this.keyboardType,
this.textCapitalization = TextCapitalization.none,
this.textInputAction,
this.obscureText = false,
this.suffixIcon,
this.onFieldSubmitted,
this.validator,
});
final TextEditingController controller;
final String label;
final IconData icon;
final TextInputType? keyboardType;
final TextCapitalization textCapitalization;
final TextInputAction? textInputAction;
final bool obscureText;
final Widget? suffixIcon;
final ValueChanged<String>? onFieldSubmitted;
final FormFieldValidator<String>? validator;
@override
Widget build(BuildContext context) {
return TextFormField(
controller: controller,
keyboardType: keyboardType,
textCapitalization: textCapitalization,
textInputAction: textInputAction,
obscureText: obscureText,
onFieldSubmitted: onFieldSubmitted,
validator: validator,
style: const TextStyle(fontSize: 15, color: AppColors.textPrimary),
decoration: InputDecoration(
labelText: label,
prefixIcon: Icon(icon, size: 20, color: AppColors.textSecondary),
suffixIcon: suffixIcon,
filled: true,
fillColor: AppColors.background,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: AppColors.border),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: AppColors.border),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: AppColors.accent, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: AppColors.cancelled, width: 1.5),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: AppColors.cancelled, width: 2),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
labelStyle: const TextStyle(color: AppColors.textSecondary, fontSize: 14),
),
);
}
}