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
@@ -16,14 +16,18 @@ class ClinicFinanceRepository {
int limit = 30,
}) async {
final filterParts = ['tenant_id = "$tenantId"', 'type = "payable"'];
if (status != null) filterParts.add('status = "$status"');
if (status == FinanceStatus.pending.value) {
filterParts.add('(status = "pending" || status = "reported")');
} else if (status != null) {
filterParts.add('status = "$status"');
}
final result = await _pb.collection('finance_entries').getList(
page: page,
perPage: limit,
filter: filterParts.join(' && '),
expand: 'job_id',
);
page: page,
perPage: limit,
filter: filterParts.join(' && '),
expand: 'job_id',
);
return (result.items.map((r) => FinanceEntry.fromJson(r.toJson())).toList()
..sort((a, b) => (b.dateCreated ?? '').compareTo(a.dateCreated ?? '')));
}
@@ -32,7 +36,7 @@ class ClinicFinanceRepository {
final all = await listEntries(tenantId, limit: 200);
double pending = 0, paid = 0;
for (final e in all) {
if (e.status == FinanceStatus.pending) {
if (e.status.isOpen) {
pending += e.amount;
} else {
paid += e.amount;
@@ -41,15 +45,17 @@ class ClinicFinanceRepository {
return {'pending': pending, 'paid': paid};
}
Future<List<CounterpartyFinanceSummary>> byCounterparty(String tenantId) async {
Future<List<CounterpartyFinanceSummary>> byCounterparty(
String tenantId) async {
final entries = await listEntries(tenantId, limit: 300);
final map = <String, CounterpartyFinanceSummary>{};
for (final entry in entries) {
final key = entry.counterpartyTenantId ?? entry.counterpartyName ?? 'unknown';
final key =
entry.counterpartyTenantId ?? entry.counterpartyName ?? 'unknown';
final current = map[key];
final pending = (current?.pendingAmount ?? 0) +
(entry.status == FinanceStatus.pending ? entry.amount : 0);
(entry.status.isOpen ? entry.amount : 0);
final paid = (current?.paidAmount ?? 0) +
(entry.status == FinanceStatus.paid ? entry.amount : 0);
map[key] = CounterpartyFinanceSummary(
@@ -67,16 +73,16 @@ class ClinicFinanceRepository {
return list;
}
Future<void> markPaid(String entryId) async {
Future<void> reportPayment(String entryId) async {
final record = await _pb.collection('finance_entries').getOne(entryId);
final jobId = record.data['job_id']?.toString();
if (jobId == null || jobId.isEmpty) {
await _pb.collection('finance_entries').update(entryId, body: {
'status': 'paid',
'paid_at': DateTime.now().toIso8601String(),
'status': 'reported',
'paid_at': null,
});
return;
}
await FinanceService.instance.markJobPaid(jobId);
await FinanceService.instance.reportJobPayment(jobId);
}
}
@@ -101,8 +101,7 @@ class _ClinicFinanceScreenState extends ConsumerState<ClinicFinanceScreen>
future: _headerFuture,
builder: (ctx, snap) {
if (snap.connectionState == ConnectionState.waiting) {
return const LinearProgressIndicator(
color: AppColors.accent);
return const LinearProgressIndicator(color: AppColors.accent);
}
final data = snap.data ??
const _ClinicFinanceHeaderData(
@@ -117,7 +116,7 @@ class _ClinicFinanceScreenState extends ConsumerState<ClinicFinanceScreen>
children: [
Expanded(
child: _SummaryCard(
label: s.pendingReceivable,
label: 'Açık Borç',
amount: data.summary['pending'] ?? 0.0,
currencyCode: currencyCode,
color: AppColors.pending,
@@ -128,7 +127,7 @@ class _ClinicFinanceScreenState extends ConsumerState<ClinicFinanceScreen>
const SizedBox(width: 12),
Expanded(
child: _SummaryCard(
label: s.collected,
label: 'Onaylanan Ödeme',
amount: data.summary['paid'] ?? 0.0,
currencyCode: currencyCode,
color: AppColors.success,
@@ -230,8 +229,7 @@ class _SummaryCard extends StatelessWidget {
width: 44,
height: 44,
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(12)),
color: bgColor, borderRadius: BorderRadius.circular(12)),
child: Icon(icon, color: color, size: 22),
),
const SizedBox(width: 12),
@@ -304,12 +302,10 @@ class _FinanceTabState extends ConsumerState<_FinanceTab> {
switch (widget.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;
@@ -323,15 +319,15 @@ class _FinanceTabState extends ConsumerState<_FinanceTab> {
return list;
}
Future<void> _markPaid(FinanceEntry entry) async {
Future<void> _reportPayment(FinanceEntry entry) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Ödeme Onayı'),
title: const Text('Ödeme Bildir'),
content: Text(
'${entry.counterpartyName ?? "Bu kayıt"} için '
'${CurrencyFormatter.format(entry.amount, widget.currencyCode)} tutarındaki borcu '
'ödendi olarak işaretlemek istiyor musunuz?',
'laboratuvara ödendi olarak bildirmek istiyor musunuz?',
),
actions: [
TextButton(
@@ -340,19 +336,21 @@ class _FinanceTabState extends ConsumerState<_FinanceTab> {
),
FilledButton(
onPressed: () => Navigator.pop(ctx, true),
child: const Text('Ödendi'),
child: const Text('Bildir'),
),
],
),
);
if (confirmed != true || !mounted) return;
try {
await ClinicFinanceRepository.instance.markPaid(entry.id);
await ClinicFinanceRepository.instance.reportPayment(entry.id);
_load();
widget.onPaymentMade();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Ödeme kaydedildi.')),
const SnackBar(
content: Text('Ödeme bildirildi. Laboratuvar onayı bekleniyor.'),
),
);
}
} catch (e) {
@@ -392,8 +390,7 @@ class _FinanceTabState extends ConsumerState<_FinanceTab> {
),
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,
@@ -437,10 +434,17 @@ class _FinanceTabState extends ConsumerState<_FinanceTab> {
itemBuilder: (context, index) {
final entry = entries[index];
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),
@@ -448,7 +452,7 @@ class _FinanceTabState extends ConsumerState<_FinanceTab> {
color: AppColors.surface,
borderRadius: BorderRadius.circular(14),
child: InkWell(
onTap: isPending ? () => _markPaid(entry) : null,
onTap: isPending ? () => _reportPayment(entry) : null,
borderRadius: BorderRadius.circular(14),
child: Container(
padding: const EdgeInsets.all(14),
@@ -472,7 +476,9 @@ class _FinanceTabState extends ConsumerState<_FinanceTab> {
child: Icon(
isPending
? Icons.hourglass_empty_rounded
: Icons.check_circle_outline,
: isReported
? Icons.verified_outlined
: Icons.check_circle_outline,
color: statusColor,
size: 22,
),
@@ -486,8 +492,7 @@ class _FinanceTabState extends ConsumerState<_FinanceTab> {
children: [
Expanded(
child: Text(
entry.counterpartyName ??
'Bilinmiyor',
entry.counterpartyName ?? 'Bilinmiyor',
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
@@ -522,6 +527,25 @@ class _FinanceTabState extends ConsumerState<_FinanceTab> {
color: AppColors.textMuted),
),
],
if (isReported) ...[
const SizedBox(height: 4),
const Text(
'Ödeme bildirildi, laboratuvar onayı bekleniyor.',
style: TextStyle(
fontSize: 12,
color: AppColors.textSecondary,
),
),
] else if (isPending) ...[
const SizedBox(height: 4),
const Text(
'Dokunarak ödeme bildirimi yapabilirsiniz.',
style: TextStyle(
fontSize: 12,
color: AppColors.textSecondary,
),
),
],
],
),
),