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
This commit is contained in:
@@ -0,0 +1,717 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
import '../../../models/job.dart';
|
||||
import '../../../models/patient.dart';
|
||||
import '../jobs/clinic_jobs_repository.dart';
|
||||
import 'clinic_patients_repository.dart';
|
||||
|
||||
class ClinicPatientDetailScreen extends ConsumerStatefulWidget {
|
||||
const ClinicPatientDetailScreen({super.key, required this.patientId});
|
||||
final String patientId;
|
||||
|
||||
@override
|
||||
ConsumerState<ClinicPatientDetailScreen> createState() =>
|
||||
_ClinicPatientDetailScreenState();
|
||||
}
|
||||
|
||||
class _ClinicPatientDetailScreenState
|
||||
extends ConsumerState<ClinicPatientDetailScreen> {
|
||||
late Future<Patient> _future;
|
||||
late Future<List<Job>> _jobsFuture;
|
||||
bool _editMode = false;
|
||||
bool _isSaving = false;
|
||||
|
||||
final _patientCodeController = TextEditingController();
|
||||
final _firstNameController = TextEditingController();
|
||||
final _lastNameController = TextEditingController();
|
||||
final _phoneController = TextEditingController();
|
||||
final _notesController = TextEditingController();
|
||||
String? _birthDate;
|
||||
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_load();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_patientCodeController.dispose();
|
||||
_firstNameController.dispose();
|
||||
_lastNameController.dispose();
|
||||
_phoneController.dispose();
|
||||
_notesController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _load() {
|
||||
setState(() {
|
||||
_future = ClinicPatientsRepository.instance
|
||||
.getPatient(widget.patientId)
|
||||
.then((p) {
|
||||
_populateControllers(p);
|
||||
return p;
|
||||
});
|
||||
_jobsFuture = ClinicJobsRepository.instance
|
||||
.listJobsByPatient(widget.patientId);
|
||||
});
|
||||
}
|
||||
|
||||
void _populateControllers(Patient p) {
|
||||
_patientCodeController.text = p.patientCode;
|
||||
_firstNameController.text = p.firstName ?? '';
|
||||
_lastNameController.text = p.lastName ?? '';
|
||||
_phoneController.text = p.phone ?? '';
|
||||
_notesController.text = p.notes ?? '';
|
||||
_birthDate = p.birthDate;
|
||||
}
|
||||
|
||||
Future<void> _pickBirthDate() async {
|
||||
DateTime initial = DateTime(1990);
|
||||
if (_birthDate != null) {
|
||||
try {
|
||||
initial = DateTime.parse(_birthDate!);
|
||||
} catch (_) {}
|
||||
}
|
||||
final picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: initial,
|
||||
firstDate: DateTime(1900),
|
||||
lastDate: DateTime.now(),
|
||||
);
|
||||
if (picked != null) {
|
||||
setState(() {
|
||||
_birthDate = picked.toIso8601String().split('T').first;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _save() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
setState(() => _isSaving = true);
|
||||
try {
|
||||
final patch = <String, dynamic>{
|
||||
'patient_code': _patientCodeController.text.trim(),
|
||||
'first_name': _firstNameController.text.trim().isNotEmpty
|
||||
? _firstNameController.text.trim()
|
||||
: null,
|
||||
'last_name': _lastNameController.text.trim().isNotEmpty
|
||||
? _lastNameController.text.trim()
|
||||
: null,
|
||||
'phone': _phoneController.text.trim().isNotEmpty
|
||||
? _phoneController.text.trim()
|
||||
: null,
|
||||
'birth_date': _birthDate,
|
||||
'notes': _notesController.text.trim().isNotEmpty
|
||||
? _notesController.text.trim()
|
||||
: null,
|
||||
};
|
||||
final updated = await ClinicPatientsRepository.instance
|
||||
.updatePatient(widget.patientId, patch);
|
||||
_populateControllers(updated);
|
||||
setState(() {
|
||||
_editMode = false;
|
||||
_future = Future.value(updated);
|
||||
});
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Hasta bilgileri güncellendi.')),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Hata: $e')),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _isSaving = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _delete() async {
|
||||
// Check for existing jobs first
|
||||
List<Job>? jobs;
|
||||
try {
|
||||
jobs = await ClinicJobsRepository.instance
|
||||
.listJobsByPatient(widget.patientId, limit: 1);
|
||||
} catch (_) {
|
||||
jobs = null;
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
final hasJobs = (jobs?.isNotEmpty) ?? false;
|
||||
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (_) => AlertDialog(
|
||||
title: const Text('Hastayı Sil'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Bu hastayı silmek istediğinizden emin misiniz?'),
|
||||
if (hasJobs) ...[
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.cancelledBg,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.warning_amber_rounded,
|
||||
color: AppColors.cancelled, size: 18),
|
||||
SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Bu hastaya ait işler bulunmaktadır. Hasta silinirse bu bağlantı kopar.',
|
||||
style: TextStyle(
|
||||
fontSize: 13, color: AppColors.cancelled),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('Vazgeç')),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
style:
|
||||
FilledButton.styleFrom(backgroundColor: AppColors.cancelled),
|
||||
child: const Text('Sil'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed != true || !mounted) return;
|
||||
|
||||
try {
|
||||
await ClinicPatientsRepository.instance.deletePatient(widget.patientId);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Hasta silindi.')),
|
||||
);
|
||||
Navigator.of(context).pop(true); // signal that a delete happened
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Silme hatası: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Hasta Detayı'),
|
||||
actions: [
|
||||
if (!_editMode) ...[
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
tooltip: 'Düzenle',
|
||||
onPressed: () => setState(() => _editMode = true),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete_outline, color: AppColors.cancelled),
|
||||
tooltip: 'Sil',
|
||||
onPressed: _delete,
|
||||
),
|
||||
] else ...[
|
||||
if (_isSaving)
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(12),
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
)
|
||||
else ...[
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
setState(() => _editMode = false);
|
||||
_future.then(_populateControllers);
|
||||
},
|
||||
child: const Text('İptal'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: _save,
|
||||
child: const Text('Kaydet'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
body: FutureBuilder<Patient>(
|
||||
future: _future,
|
||||
builder: (ctx, snap) {
|
||||
if (snap.connectionState == ConnectionState.waiting) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (snap.hasError) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('Hata: ${snap.error}'),
|
||||
const SizedBox(height: 12),
|
||||
FilledButton(
|
||||
onPressed: _load,
|
||||
child: const Text('Tekrar Dene'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_editMode) {
|
||||
return _EditForm(
|
||||
formKey: _formKey,
|
||||
patientCodeController: _patientCodeController,
|
||||
firstNameController: _firstNameController,
|
||||
lastNameController: _lastNameController,
|
||||
phoneController: _phoneController,
|
||||
notesController: _notesController,
|
||||
birthDate: _birthDate,
|
||||
onPickBirthDate: _pickBirthDate,
|
||||
);
|
||||
}
|
||||
|
||||
final patient = snap.data!;
|
||||
return _PatientView(patient: patient, jobsFuture: _jobsFuture);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── View ───────────────────────────────────────────────────────────────────
|
||||
|
||||
class _PatientView extends StatelessWidget {
|
||||
const _PatientView({required this.patient, required this.jobsFuture});
|
||||
final Patient patient;
|
||||
final Future<List<Job>> jobsFuture;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
// Avatar + name header
|
||||
Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 72,
|
||||
height: 72,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.inProgressBg,
|
||||
borderRadius: BorderRadius.circular(20)),
|
||||
child: Center(
|
||||
child: Text(
|
||||
patient.displayName.isNotEmpty
|
||||
? patient.displayName[0].toUpperCase()
|
||||
: '?',
|
||||
style: const TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: AppColors.inProgress),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
patient.displayName,
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
patient.patientCode,
|
||||
style: const TextStyle(
|
||||
fontSize: 13, color: AppColors.textSecondary),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColors.border),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4))
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_DetailRow(label: 'Hasta Kodu', value: patient.patientCode),
|
||||
if (patient.firstName != null)
|
||||
_DetailRow(label: 'Ad', value: patient.firstName!),
|
||||
if (patient.lastName != null)
|
||||
_DetailRow(label: 'Soyad', value: patient.lastName!),
|
||||
if (patient.phone != null && patient.phone!.isNotEmpty)
|
||||
_DetailRow(label: 'Telefon', value: patient.phone!),
|
||||
if (patient.birthDate != null && patient.birthDate!.isNotEmpty)
|
||||
_DetailRow(
|
||||
label: 'Doğum Tarihi', value: patient.birthDate!),
|
||||
if (patient.notes != null && patient.notes!.isNotEmpty)
|
||||
_DetailRow(label: 'Notlar', value: patient.notes!),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Job history
|
||||
_JobHistory(jobsFuture: jobsFuture),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Job history ────────────────────────────────────────────────────────────
|
||||
|
||||
class _JobHistory extends StatelessWidget {
|
||||
const _JobHistory({required this.jobsFuture});
|
||||
final Future<List<Job>> jobsFuture;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'İŞ GEÇMİŞİ',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.textSecondary,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
FutureBuilder<List<Job>>(
|
||||
future: jobsFuture,
|
||||
builder: (ctx, snap) {
|
||||
if (snap.connectionState == ConnectionState.waiting) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (snap.hasError) {
|
||||
return Text('Yüklenemedi: ${snap.error}',
|
||||
style:
|
||||
const TextStyle(color: AppColors.textSecondary));
|
||||
}
|
||||
final jobs = snap.data ?? [];
|
||||
if (jobs.isEmpty) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColors.border),
|
||||
),
|
||||
child: const Center(
|
||||
child: Text(
|
||||
'Henüz iş kaydı yok.',
|
||||
style: TextStyle(color: AppColors.textSecondary),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColors.border),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4))
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: jobs.asMap().entries.map((entry) {
|
||||
final i = entry.key;
|
||||
final job = entry.value;
|
||||
final isLast = i == jobs.length - 1;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: _statusBg(job.status),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
_statusIcon(job.status),
|
||||
color: _statusColor(job.status),
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
job.patientCode,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${job.prostheticType.label} · ${_statusLabel(job.status)}',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_formatDate(job.dateCreated),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (!isLast)
|
||||
const Divider(
|
||||
height: 1,
|
||||
indent: 68,
|
||||
color: AppColors.border),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
static Color _statusBg(JobStatus s) => switch (s) {
|
||||
JobStatus.delivered => AppColors.successBg,
|
||||
JobStatus.cancelled => AppColors.cancelledBg,
|
||||
JobStatus.inProgress => AppColors.inProgressBg,
|
||||
_ => AppColors.pendingBg,
|
||||
};
|
||||
|
||||
static Color _statusColor(JobStatus s) => switch (s) {
|
||||
JobStatus.delivered => AppColors.success,
|
||||
JobStatus.cancelled => AppColors.cancelled,
|
||||
JobStatus.inProgress => AppColors.inProgress,
|
||||
_ => AppColors.pending,
|
||||
};
|
||||
|
||||
static IconData _statusIcon(JobStatus s) => switch (s) {
|
||||
JobStatus.delivered => Icons.check_circle_outline,
|
||||
JobStatus.cancelled => Icons.cancel_outlined,
|
||||
JobStatus.inProgress => Icons.autorenew,
|
||||
_ => Icons.hourglass_empty_outlined,
|
||||
};
|
||||
|
||||
static String _statusLabel(JobStatus s) => switch (s) {
|
||||
JobStatus.pending => 'Bekliyor',
|
||||
JobStatus.inProgress => 'Üretimde',
|
||||
JobStatus.sent => 'Gönderildi',
|
||||
JobStatus.delivered => 'Teslim Edildi',
|
||||
JobStatus.cancelled => 'İptal',
|
||||
};
|
||||
|
||||
static String _formatDate(DateTime d) {
|
||||
return '${d.day.toString().padLeft(2, '0')}.${d.month.toString().padLeft(2, '0')}.${d.year}';
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
class _DetailRow extends StatelessWidget {
|
||||
const _DetailRow({required this.label, required this.value});
|
||||
final String label;
|
||||
final String value;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 120,
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 13, color: AppColors.textSecondary),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.textPrimary),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Edit form ──────────────────────────────────────────────────────────────
|
||||
|
||||
class _EditForm extends StatelessWidget {
|
||||
const _EditForm({
|
||||
required this.formKey,
|
||||
required this.patientCodeController,
|
||||
required this.firstNameController,
|
||||
required this.lastNameController,
|
||||
required this.phoneController,
|
||||
required this.notesController,
|
||||
required this.birthDate,
|
||||
required this.onPickBirthDate,
|
||||
});
|
||||
|
||||
final GlobalKey<FormState> formKey;
|
||||
final TextEditingController patientCodeController;
|
||||
final TextEditingController firstNameController;
|
||||
final TextEditingController lastNameController;
|
||||
final TextEditingController phoneController;
|
||||
final TextEditingController notesController;
|
||||
final String? birthDate;
|
||||
final VoidCallback onPickBirthDate;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: patientCodeController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Hasta Kodu *',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
validator: (val) =>
|
||||
(val == null || val.trim().isEmpty)
|
||||
? 'Hasta kodu zorunludur'
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: firstNameController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Ad',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: lastNameController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Soyad',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: phoneController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Telefon',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: TextInputType.phone,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
InkWell(
|
||||
onTap: onPickBirthDate,
|
||||
child: InputDecorator(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Doğum Tarihi',
|
||||
border: OutlineInputBorder(),
|
||||
suffixIcon: Icon(Icons.calendar_today),
|
||||
),
|
||||
child: Text(
|
||||
birthDate ?? 'Tarih seçin',
|
||||
style: birthDate != null
|
||||
? null
|
||||
: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: notesController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Notlar',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
minLines: 3,
|
||||
maxLines: 5,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
import '../../../core/api/pocketbase_client.dart';
|
||||
import '../../../models/patient.dart';
|
||||
|
||||
class ClinicPatientsRepository {
|
||||
ClinicPatientsRepository._();
|
||||
static final instance = ClinicPatientsRepository._();
|
||||
|
||||
PocketBase get _pb => PocketBaseClient.instance.pb;
|
||||
|
||||
Future<List<Patient>> listPatients(
|
||||
String tenantId, {
|
||||
String? search,
|
||||
int page = 1,
|
||||
int limit = 30,
|
||||
}) async {
|
||||
final filterParts = ['tenant_id = "$tenantId"'];
|
||||
if (search != null && search.isNotEmpty) {
|
||||
filterParts.add(
|
||||
'(patient_code ~ "$search" || first_name ~ "$search" || last_name ~ "$search")',
|
||||
);
|
||||
}
|
||||
|
||||
final result = await _pb.collection('patients').getList(
|
||||
page: page,
|
||||
perPage: limit,
|
||||
filter: filterParts.join(' && '),
|
||||
);
|
||||
return (result.items.map((r) => Patient.fromJson(r.toJson())).toList()
|
||||
..sort((a, b) => a.patientCode.compareTo(b.patientCode)));
|
||||
}
|
||||
|
||||
Future<Patient> getPatient(String patientId) async {
|
||||
final record = await _pb.collection('patients').getOne(patientId);
|
||||
return Patient.fromJson(record.toJson());
|
||||
}
|
||||
|
||||
Future<Patient> createPatient({
|
||||
required String tenantId,
|
||||
required String patientCode,
|
||||
String? firstName,
|
||||
String? lastName,
|
||||
String? birthDate,
|
||||
String? phone,
|
||||
String? notes,
|
||||
}) async {
|
||||
final record = await _pb.collection('patients').create(body: {
|
||||
'tenant_id': tenantId,
|
||||
'patient_code': patientCode,
|
||||
if (firstName != null) 'first_name': firstName,
|
||||
if (lastName != null) 'last_name': lastName,
|
||||
if (birthDate != null) 'birth_date': birthDate,
|
||||
if (phone != null) 'phone': phone,
|
||||
if (notes != null) 'notes': notes,
|
||||
});
|
||||
return Patient.fromJson(record.toJson());
|
||||
}
|
||||
|
||||
Future<Patient> updatePatient(String patientId, Map<String, dynamic> patch) async {
|
||||
final record = await _pb.collection('patients').update(patientId, body: patch);
|
||||
return Patient.fromJson(record.toJson());
|
||||
}
|
||||
|
||||
Future<void> deletePatient(String patientId) async {
|
||||
await _pb.collection('patients').delete(patientId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,576 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../../core/providers/auth_provider.dart';
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
import '../../../core/widgets/gradient_app_bar.dart';
|
||||
import '../../../models/patient.dart';
|
||||
import 'clinic_patients_repository.dart';
|
||||
|
||||
enum _PatientSort { nameAZ, nameZA, byCode }
|
||||
|
||||
const _kSortLabels = [
|
||||
'Ada Göre (A → Z)',
|
||||
'Ada Göre (Z → A)',
|
||||
'Hasta Koduna Göre',
|
||||
];
|
||||
|
||||
void _showAdaptive(BuildContext context, Widget content) {
|
||||
final isDesktop =
|
||||
MediaQuery.sizeOf(context).width > AppLayout.sidebarBreakpoint;
|
||||
if (isDesktop) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) => Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 560),
|
||||
child: content,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (_) => content,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ClinicPatientsScreen extends ConsumerStatefulWidget {
|
||||
const ClinicPatientsScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<ClinicPatientsScreen> createState() =>
|
||||
_ClinicPatientsScreenState();
|
||||
}
|
||||
|
||||
class _ClinicPatientsScreenState extends ConsumerState<ClinicPatientsScreen> {
|
||||
late Future<List<Patient>> _future;
|
||||
final _searchController = TextEditingController();
|
||||
String _searchQuery = '';
|
||||
_PatientSort _sort = _PatientSort.nameAZ;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_load();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _load([String? search]) {
|
||||
final tenantId = ref.read(authProvider).activeTenant!.tenant.id;
|
||||
setState(() {
|
||||
_future = ClinicPatientsRepository.instance.listPatients(
|
||||
tenantId,
|
||||
search: search?.trim().isNotEmpty == true ? search : null,
|
||||
limit: 100,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _onSearchChanged(String value) {
|
||||
setState(() => _searchQuery = value);
|
||||
_load(value);
|
||||
}
|
||||
|
||||
Future<void> _showSortOptions() async {
|
||||
final result = await showSortSheet(
|
||||
context,
|
||||
title: 'Sıralama',
|
||||
options: _kSortLabels,
|
||||
current: _sort.index,
|
||||
);
|
||||
if (result != null) {
|
||||
setState(() => _sort = _PatientSort.values[result]);
|
||||
}
|
||||
}
|
||||
|
||||
List<Patient> _sorted(List<Patient> patients) {
|
||||
final list = List<Patient>.from(patients);
|
||||
switch (_sort) {
|
||||
case _PatientSort.nameAZ:
|
||||
list.sort((a, b) => a.displayName.compareTo(b.displayName));
|
||||
case _PatientSort.nameZA:
|
||||
list.sort((a, b) => b.displayName.compareTo(a.displayName));
|
||||
case _PatientSort.byCode:
|
||||
list.sort((a, b) => a.patientCode.compareTo(b.patientCode));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
void _showNewPatientSheet() {
|
||||
_showAdaptive(
|
||||
context,
|
||||
_NewPatientSheet(
|
||||
onCreated: () {
|
||||
_load(_searchQuery);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Hasta oluşturuldu.')),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isSortActive = _sort != _PatientSort.nameAZ;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.background,
|
||||
appBar: GradientAppBar(
|
||||
title: 'Hastalar',
|
||||
category: 'KLİNİK',
|
||||
searchController: _searchController,
|
||||
onSearchChanged: _onSearchChanged,
|
||||
searchHint: 'Ad, soyad veya kod ile arayın...',
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: _showSortOptions,
|
||||
tooltip: 'Sırala',
|
||||
icon: Badge(
|
||||
isLabelVisible: isSortActive,
|
||||
smallSize: 8,
|
||||
backgroundColor: AppColors.accent,
|
||||
child: const Icon(Icons.sort_rounded),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _showNewPatientSheet,
|
||||
tooltip: 'Yeni Hasta',
|
||||
icon: const Icon(Icons.person_add_outlined),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
color: AppColors.accent,
|
||||
onRefresh: () async => _load(_searchQuery),
|
||||
child: FutureBuilder<List<Patient>>(
|
||||
future: _future,
|
||||
builder: (ctx, snap) {
|
||||
if (snap.connectionState == ConnectionState.waiting) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: AppColors.accent));
|
||||
}
|
||||
if (snap.hasError) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 64,
|
||||
height: 64,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.cancelledBg,
|
||||
borderRadius: BorderRadius.circular(16)),
|
||||
child: const Icon(Icons.wifi_off_rounded,
|
||||
color: AppColors.cancelled, size: 30),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text('Hata: ${snap.error}',
|
||||
style: const TextStyle(
|
||||
color: AppColors.textSecondary)),
|
||||
const SizedBox(height: 12),
|
||||
FilledButton.icon(
|
||||
onPressed: () => _load(_searchQuery),
|
||||
icon: const Icon(Icons.refresh_rounded, size: 18),
|
||||
label: const Text('Tekrar Dene'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
final patients = _sorted(snap.data!);
|
||||
if (patients.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 72,
|
||||
height: 72,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.inProgressBg,
|
||||
borderRadius: BorderRadius.circular(20)),
|
||||
child: const Icon(Icons.people_outline,
|
||||
color: AppColors.inProgress, size: 32),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
_searchQuery.isNotEmpty
|
||||
? 'Sonuç bulunamadı'
|
||||
: 'Henüz hasta yok',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textPrimary),
|
||||
),
|
||||
if (_searchQuery.isEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Yeni hasta eklemek için + düğmesine dokunun',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: AppColors.textSecondary),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 24),
|
||||
itemCount: patients.length,
|
||||
itemBuilder: (context, index) {
|
||||
final patient = patients[index];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: _PatientCard(
|
||||
patient: patient,
|
||||
onTap: () => context
|
||||
.push('/clinic/patients/${patient.id}'),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PatientCard extends StatelessWidget {
|
||||
const _PatientCard({required this.patient, required this.onTap});
|
||||
|
||||
final Patient patient;
|
||||
final VoidCallback onTap;
|
||||
|
||||
String get _initials {
|
||||
final name = patient.displayName;
|
||||
if (name.isEmpty) return '?';
|
||||
final parts = name.trim().split(' ');
|
||||
if (parts.length >= 2) {
|
||||
return '${parts.first[0]}${parts.last[0]}'.toUpperCase();
|
||||
}
|
||||
return name[0].toUpperCase();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: AppColors.border),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.04),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2))
|
||||
]),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 46,
|
||||
height: 46,
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0xFF1E3A5F), Color(0xFF0369A1)],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(13),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
_initials,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
patient.displayName.isNotEmpty
|
||||
? patient.displayName
|
||||
: patient.patientCode,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textPrimary),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surfaceVariant,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
patient.patientCode,
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
color: AppColors.textSecondary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (patient.phone != null &&
|
||||
patient.phone!.isNotEmpty) ...[
|
||||
const SizedBox(width: 8),
|
||||
const Icon(Icons.phone_outlined,
|
||||
size: 11, color: AppColors.textMuted),
|
||||
const SizedBox(width: 3),
|
||||
Text(
|
||||
patient.phone!,
|
||||
style: const TextStyle(
|
||||
fontSize: 11, color: AppColors.textMuted),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(Icons.chevron_right,
|
||||
color: AppColors.textMuted, size: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── New Patient Sheet ─────────────────────────────────────────────────────────
|
||||
|
||||
class _NewPatientSheet extends ConsumerStatefulWidget {
|
||||
const _NewPatientSheet({required this.onCreated});
|
||||
|
||||
final VoidCallback onCreated;
|
||||
|
||||
@override
|
||||
ConsumerState<_NewPatientSheet> createState() => _NewPatientSheetState();
|
||||
}
|
||||
|
||||
class _NewPatientSheetState extends ConsumerState<_NewPatientSheet> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _patientCodeController = TextEditingController();
|
||||
final _firstNameController = TextEditingController();
|
||||
final _lastNameController = TextEditingController();
|
||||
final _phoneController = TextEditingController();
|
||||
final _notesController = TextEditingController();
|
||||
String? _birthDate;
|
||||
bool _isSubmitting = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_patientCodeController.dispose();
|
||||
_firstNameController.dispose();
|
||||
_lastNameController.dispose();
|
||||
_phoneController.dispose();
|
||||
_notesController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _pickBirthDate() async {
|
||||
final picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: DateTime(1990),
|
||||
firstDate: DateTime(1900),
|
||||
lastDate: DateTime.now(),
|
||||
);
|
||||
if (picked != null) {
|
||||
setState(() {
|
||||
_birthDate = picked.toIso8601String().split('T').first;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
setState(() => _isSubmitting = true);
|
||||
try {
|
||||
final tenantId = ref.read(authProvider).activeTenant!.tenant.id;
|
||||
await ClinicPatientsRepository.instance.createPatient(
|
||||
tenantId: tenantId,
|
||||
patientCode: _patientCodeController.text.trim(),
|
||||
firstName: _firstNameController.text.trim().isNotEmpty
|
||||
? _firstNameController.text.trim()
|
||||
: null,
|
||||
lastName: _lastNameController.text.trim().isNotEmpty
|
||||
? _lastNameController.text.trim()
|
||||
: null,
|
||||
phone: _phoneController.text.trim().isNotEmpty
|
||||
? _phoneController.text.trim()
|
||||
: null,
|
||||
birthDate: _birthDate,
|
||||
notes: _notesController.text.trim().isNotEmpty
|
||||
? _notesController.text.trim()
|
||||
: null,
|
||||
);
|
||||
widget.onCreated();
|
||||
// Pop using form's own context (inside dialog) to ensure correct Navigator
|
||||
if (mounted) Navigator.of(context).pop();
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Hata: $e')),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _isSubmitting = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDesktop =
|
||||
MediaQuery.sizeOf(context).width > AppLayout.sidebarBreakpoint;
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: isDesktop ? Radius.zero : const Radius.circular(20),
|
||||
),
|
||||
),
|
||||
padding: EdgeInsets.only(
|
||||
bottom: isDesktop ? 0 : MediaQuery.of(context).viewInsets.bottom,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Yeni Hasta',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _patientCodeController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Hasta Kodu *',
|
||||
hintText: 'Ör: P-001',
|
||||
),
|
||||
validator: (val) =>
|
||||
(val == null || val.trim().isEmpty)
|
||||
? 'Hasta kodu zorunludur'
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _firstNameController,
|
||||
decoration: const InputDecoration(labelText: 'Ad'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _lastNameController,
|
||||
decoration: const InputDecoration(labelText: 'Soyad'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: _phoneController,
|
||||
decoration: const InputDecoration(labelText: 'Telefon'),
|
||||
keyboardType: TextInputType.phone,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
InkWell(
|
||||
onTap: _pickBirthDate,
|
||||
child: InputDecorator(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Doğum Tarihi',
|
||||
suffixIcon: Icon(Icons.calendar_today),
|
||||
),
|
||||
child: Text(
|
||||
_birthDate ?? 'Tarih seçin',
|
||||
style: _birthDate != null
|
||||
? null
|
||||
: const TextStyle(color: AppColors.textMuted),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: _notesController,
|
||||
decoration: const InputDecoration(labelText: 'Notlar'),
|
||||
minLines: 2,
|
||||
maxLines: 3,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
if (_isSubmitting)
|
||||
const Center(
|
||||
child: CircularProgressIndicator(color: AppColors.accent))
|
||||
else
|
||||
FilledButton(
|
||||
onPressed: _submit,
|
||||
style: FilledButton.styleFrom(
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
),
|
||||
child: const Text('Hasta Oluştur'),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user