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
+173 -89
View File
@@ -76,8 +76,10 @@ class _LabFinanceScreenState extends ConsumerState<LabFinanceScreen>
switch (_sort) {
case _FinanceSort.newestFirst:
list.sort((a, b) {
final da = a.dateCreated != null ? DateTime.tryParse(a.dateCreated!) : null;
final db = b.dateCreated != null ? DateTime.tryParse(b.dateCreated!) : null;
final da =
a.dateCreated != null ? DateTime.tryParse(a.dateCreated!) : null;
final db =
b.dateCreated != null ? DateTime.tryParse(b.dateCreated!) : null;
if (da == null && db == null) return 0;
if (da == null) return 1;
if (db == null) return -1;
@@ -101,6 +103,49 @@ class _LabFinanceScreenState extends ConsumerState<LabFinanceScreen>
}
}
Future<void> _confirmPayment(
FinanceEntry entry,
String Function(double) formatAmount,
) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Ödeme Onayla'),
content: Text(
'${entry.counterpartyName ?? "Bu kayıt"} için '
'${formatAmount(entry.amount)} tutarındaki ödemenin '
'hesabınıza ulaştığını onaylıyor musunuz?',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('İptal'),
),
FilledButton(
onPressed: () => Navigator.pop(ctx, true),
child: const Text('Onayla'),
),
],
),
);
if (confirmed != true || !mounted) return;
try {
await LabFinanceRepository.instance.confirmPayment(entry.id);
_load();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Ödeme onaylandı.')),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Hata: $e')),
);
}
}
}
@override
Widget build(BuildContext context) {
final isSortActive = _sort != _FinanceSort.newestFirst;
@@ -154,8 +199,7 @@ class _LabFinanceScreenState extends ConsumerState<LabFinanceScreen>
),
const SizedBox(height: 16),
Text('Hata: ${snap.error}',
style: const TextStyle(
color: AppColors.textSecondary)),
style: const TextStyle(color: AppColors.textSecondary)),
const SizedBox(height: 12),
FilledButton.icon(
onPressed: _load,
@@ -181,7 +225,7 @@ class _LabFinanceScreenState extends ConsumerState<LabFinanceScreen>
children: [
Expanded(
child: _SummaryCard(
label: s.pendingReceivable,
label: 'Açık Alacak',
amount: formatAmount(pendingTotal),
color: AppColors.pending,
bgColor: AppColors.pendingBg,
@@ -191,7 +235,7 @@ class _LabFinanceScreenState extends ConsumerState<LabFinanceScreen>
const SizedBox(width: 12),
Expanded(
child: _SummaryCard(
label: s.collected,
label: 'Onaylanan Tahsilat',
amount: formatAmount(paidTotal),
color: AppColors.success,
bgColor: AppColors.successBg,
@@ -226,6 +270,8 @@ class _LabFinanceScreenState extends ConsumerState<LabFinanceScreen>
emptyIcon: Icons.hourglass_empty_rounded,
formatDate: _formatDate,
formatAmount: formatAmount,
onConfirmPayment: (entry) =>
_confirmPayment(entry, formatAmount),
),
_EntriesList(
entries: paid,
@@ -334,6 +380,7 @@ class _EntriesList extends StatelessWidget {
required this.emptyIcon,
required this.formatDate,
required this.formatAmount,
this.onConfirmPayment,
});
final List<FinanceEntry> entries;
@@ -341,6 +388,7 @@ class _EntriesList extends StatelessWidget {
final IconData emptyIcon;
final String Function(String?) formatDate;
final String Function(double) formatAmount;
final Future<void> Function(FinanceEntry entry)? onConfirmPayment;
@override
Widget build(BuildContext context) {
@@ -374,103 +422,139 @@ class _EntriesList extends StatelessWidget {
itemBuilder: (ctx, i) {
final entry = entries[i];
final isPending = entry.status == FinanceStatus.pending;
final statusColor = isPending ? AppColors.pending : AppColors.success;
final statusBg = isPending ? AppColors.pendingBg : AppColors.successBg;
final isReported = entry.status == FinanceStatus.reported;
final statusColor = isPending
? AppColors.pending
: isReported
? AppColors.accent
: AppColors.success;
final statusBg = isPending
? AppColors.pendingBg
: isReported
? AppColors.inProgressBg
: AppColors.successBg;
return Padding(
padding: const EdgeInsets.only(bottom: 10),
child: Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: AppColors.surface,
child: Material(
color: AppColors.surface,
borderRadius: BorderRadius.circular(14),
child: InkWell(
onTap: isReported && onConfirmPayment != null
? () => onConfirmPayment!(entry)
: null,
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: 44,
height: 44,
decoration: BoxDecoration(
color: statusBg,
borderRadius: BorderRadius.circular(12)),
child: Icon(
isPending
? Icons.hourglass_empty_rounded
: Icons.check_circle_outline,
color: statusColor,
size: 22,
),
child: Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: AppColors.surface,
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))
],
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
entry.counterpartyName ?? 'Klinik',
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary),
),
if (entry.patientCode != null) ...[
const SizedBox(height: 2),
Text(
'Protokol: ${entry.patientCode}',
style: const TextStyle(
fontSize: 12, color: AppColors.textSecondary),
),
],
if (entry.dateCreated != null) ...[
const SizedBox(height: 2),
Text(
formatDate(entry.dateCreated),
style: const TextStyle(
fontSize: 12, color: AppColors.textMuted),
),
],
],
),
),
const SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
child: Row(
children: [
Text(
formatAmount(entry.amount),
style: TextStyle(
fontWeight: FontWeight.w700,
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: statusBg,
borderRadius: BorderRadius.circular(12)),
child: Icon(
isPending
? Icons.hourglass_empty_rounded
: isReported
? Icons.verified_outlined
: Icons.check_circle_outline,
color: statusColor,
fontSize: 15,
size: 22,
),
),
const SizedBox(height: 4),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: statusBg,
borderRadius: BorderRadius.circular(8),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
entry.counterpartyName ?? 'Klinik',
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary),
),
if (entry.patientCode != null) ...[
const SizedBox(height: 2),
Text(
'Protokol: ${entry.patientCode}',
style: const TextStyle(
fontSize: 12, color: AppColors.textSecondary),
),
],
if (entry.dateCreated != null) ...[
const SizedBox(height: 2),
Text(
formatDate(entry.dateCreated),
style: const TextStyle(
fontSize: 12, color: AppColors.textMuted),
),
],
if (isReported) ...[
const SizedBox(height: 4),
const Text(
'Dokunarak ödeme onayı verebilirsiniz.',
style: TextStyle(
fontSize: 12, color: AppColors.textSecondary),
),
] else if (isPending) ...[
const SizedBox(height: 4),
const Text(
'Klinikten ödeme bildirimi bekleniyor.',
style: TextStyle(
fontSize: 12, color: AppColors.textSecondary),
),
],
],
),
child: Text(
entry.status.label,
style: TextStyle(
color: statusColor,
fontSize: 11,
fontWeight: FontWeight.w600,
),
const SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
formatAmount(entry.amount),
style: TextStyle(
fontWeight: FontWeight.w700,
color: statusColor,
fontSize: 15,
),
),
),
const SizedBox(height: 4),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: statusBg,
borderRadius: BorderRadius.circular(8),
),
child: Text(
entry.status.label,
style: TextStyle(
color: statusColor,
fontSize: 11,
fontWeight: FontWeight.w600,
),
),
),
],
),
],
),
],
),
),
),
);