feat: improve patient flow and pricing workflow
This commit is contained in:
@@ -742,14 +742,14 @@ class _DiscountSheetState extends State<_DiscountSheet> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
const Text('Minimum Sipariş Adedi (İsteğe Bağlı)',
|
||||
const Text('Minimum Faturalanabilir Adet (İsteğe Bağlı)',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textSecondary)),
|
||||
const SizedBox(height: 4),
|
||||
const Text(
|
||||
'Aylık bu adede ulaşılınca indirim devreye girer. 0 = koşulsuz.',
|
||||
'İş bazında diş/vaka adedi bu eşiğe ulaşınca indirim devreye girer. 0 = koşulsuz.',
|
||||
style:
|
||||
TextStyle(fontSize: 11, color: AppColors.textMuted)),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
@@ -39,4 +39,30 @@ class LabFinanceRepository {
|
||||
}
|
||||
return {'pending': pending, 'paid': paid};
|
||||
}
|
||||
|
||||
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 current = map[key];
|
||||
final pending = (current?.pendingAmount ?? 0) +
|
||||
(entry.status == FinanceStatus.pending ? entry.amount : 0);
|
||||
final paid = (current?.paidAmount ?? 0) +
|
||||
(entry.status == FinanceStatus.paid ? entry.amount : 0);
|
||||
map[key] = CounterpartyFinanceSummary(
|
||||
counterpartyTenantId: entry.counterpartyTenantId,
|
||||
counterpartyName: entry.counterpartyName ?? 'Karşı Taraf',
|
||||
currency: entry.currency,
|
||||
pendingAmount: pending,
|
||||
paidAmount: paid,
|
||||
entryCount: (current?.entryCount ?? 0) + 1,
|
||||
);
|
||||
}
|
||||
|
||||
final list = map.values.toList();
|
||||
list.sort((a, b) => b.pendingAmount.compareTo(a.pendingAmount));
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,10 +48,12 @@ class _LabFinanceScreenState extends ConsumerState<LabFinanceScreen>
|
||||
LabFinanceRepository.instance.listEntries(tenantId, status: 'pending'),
|
||||
LabFinanceRepository.instance.listEntries(tenantId, status: 'paid'),
|
||||
LabFinanceRepository.instance.summary(tenantId),
|
||||
LabFinanceRepository.instance.byCounterparty(tenantId),
|
||||
]).then((results) => _FinanceData(
|
||||
pending: results[0] as List<FinanceEntry>,
|
||||
paid: results[1] as List<FinanceEntry>,
|
||||
summary: results[2] as Map<String, double>,
|
||||
counterparties: results[3] as List<CounterpartyFinanceSummary>,
|
||||
));
|
||||
});
|
||||
}
|
||||
@@ -199,6 +201,15 @@ class _LabFinanceScreenState extends ConsumerState<LabFinanceScreen>
|
||||
],
|
||||
),
|
||||
),
|
||||
if (data.counterparties.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
|
||||
child: _CounterpartySummaryList(
|
||||
title: 'Klinik Bazlı Alacak',
|
||||
items: data.counterparties,
|
||||
formatAmount: formatAmount,
|
||||
),
|
||||
),
|
||||
PillTabs(
|
||||
tabs: [s.pending, s.collected],
|
||||
selected: _tabController.index,
|
||||
@@ -240,11 +251,13 @@ class _FinanceData {
|
||||
required this.pending,
|
||||
required this.paid,
|
||||
required this.summary,
|
||||
required this.counterparties,
|
||||
});
|
||||
|
||||
final List<FinanceEntry> pending;
|
||||
final List<FinanceEntry> paid;
|
||||
final Map<String, double> summary;
|
||||
final List<CounterpartyFinanceSummary> counterparties;
|
||||
}
|
||||
|
||||
class _SummaryCard extends StatelessWidget {
|
||||
@@ -465,3 +478,66 @@ class _EntriesList extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CounterpartySummaryList extends StatelessWidget {
|
||||
const _CounterpartySummaryList({
|
||||
required this.title,
|
||||
required this.items,
|
||||
required this.formatAmount,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final List<CounterpartyFinanceSummary> items;
|
||||
final String Function(double) formatAmount;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColors.border),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
for (final item in items.take(5)) ...[
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
item.counterpartyName,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: AppColors.textPrimary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
formatAmount(item.pendingAmount),
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: AppColors.pending,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,8 +273,10 @@ class _PendingJobsTabState extends ConsumerState<_PendingJobsTab> {
|
||||
if (q.isEmpty) return jobs;
|
||||
return jobs.where((j) =>
|
||||
j.patientCode.toLowerCase().contains(q) ||
|
||||
(j.patientName?.toLowerCase().contains(q) ?? false) ||
|
||||
(j.clinicName?.toLowerCase().contains(q) ?? false) ||
|
||||
j.prostheticType.label.toLowerCase().contains(q)
|
||||
j.prostheticType.label.toLowerCase().contains(q) ||
|
||||
(j.prostheticName?.toLowerCase().contains(q) ?? false)
|
||||
).toList();
|
||||
}
|
||||
|
||||
@@ -591,8 +593,10 @@ class _LabJobsTabState extends ConsumerState<_LabJobsTab> {
|
||||
if (q.isNotEmpty) {
|
||||
list = list.where((j) {
|
||||
return j.patientCode.toLowerCase().contains(q) ||
|
||||
(j.patientName?.toLowerCase().contains(q) ?? false) ||
|
||||
(j.clinicName?.toLowerCase().contains(q) ?? false) ||
|
||||
j.prostheticType.label.toLowerCase().contains(q) ||
|
||||
(j.prostheticName?.toLowerCase().contains(q) ?? false) ||
|
||||
(j.currentStep?.label.toLowerCase().contains(q) ?? false);
|
||||
}).toList();
|
||||
}
|
||||
@@ -722,12 +726,15 @@ class _LabJobCard extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final title = job.patientName?.trim().isNotEmpty == true
|
||||
? job.patientName!
|
||||
: job.patientCode;
|
||||
final isOverdue =
|
||||
job.dueDate != null && job.dueDate!.isBefore(DateTime.now());
|
||||
final accentColor = _statusColor(job.status);
|
||||
|
||||
return Semantics(
|
||||
label: job.patientCode,
|
||||
label: title,
|
||||
button: true,
|
||||
excludeSemantics: true,
|
||||
child: Material(
|
||||
@@ -771,7 +778,7 @@ class _LabJobCard extends StatelessWidget {
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
job.patientCode,
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w700,
|
||||
@@ -798,6 +805,16 @@ class _LabJobCard extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
if (job.patientName?.isNotEmpty == true) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
job.patientCode,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.textMuted,
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 5),
|
||||
Row(
|
||||
children: [
|
||||
@@ -827,7 +844,9 @@ class _LabJobCard extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
job.prostheticType.label,
|
||||
job.prostheticName?.isNotEmpty == true
|
||||
? '${job.prostheticType.label} · ${job.prostheticName}'
|
||||
: job.prostheticType.label,
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
color: AppColors.textSecondary,
|
||||
|
||||
@@ -258,7 +258,9 @@ class _LabJobDetailScreenState extends ConsumerState<LabJobDetailScreen> {
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
job.patientCode,
|
||||
job.patientName?.isNotEmpty == true
|
||||
? job.patientName!
|
||||
: job.patientCode,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.headlineSmall
|
||||
@@ -289,10 +291,40 @@ class _LabJobDetailScreenState extends ConsumerState<LabJobDetailScreen> {
|
||||
icon: Icons.business,
|
||||
label: 'Klinik',
|
||||
value: job.clinicName ?? '-'),
|
||||
if (job.patientName != null &&
|
||||
job.patientName!.isNotEmpty)
|
||||
_InfoRow(
|
||||
icon: Icons.person_outline,
|
||||
label: 'Hasta',
|
||||
value: job.patientName!,
|
||||
),
|
||||
_InfoRow(
|
||||
icon: Icons.tag_outlined,
|
||||
label: 'Protokol No',
|
||||
value: job.patientCode,
|
||||
),
|
||||
_InfoRow(
|
||||
icon: Icons.medical_services_outlined,
|
||||
label: 'Protez Tipi',
|
||||
value: job.prostheticType.label),
|
||||
if (job.prostheticName != null &&
|
||||
job.prostheticName!.isNotEmpty)
|
||||
_InfoRow(
|
||||
icon: Icons.category_outlined,
|
||||
label: 'Ürün',
|
||||
value: job.prostheticName!,
|
||||
),
|
||||
if (job.workflowType != null)
|
||||
_InfoRow(
|
||||
icon: Icons.tune_rounded,
|
||||
label: 'İş Tipi',
|
||||
value: job.workflowType!.label,
|
||||
),
|
||||
_InfoRow(
|
||||
icon: Icons.fact_check_outlined,
|
||||
label: 'Prova',
|
||||
value: job.provaRequired ? 'Provalı' : 'Provasız',
|
||||
),
|
||||
_InfoRow(
|
||||
icon: Icons.format_list_numbered,
|
||||
label: 'Üye Sayısı',
|
||||
@@ -761,4 +793,3 @@ class _JobStepper extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -188,8 +188,11 @@ class _InboundJobCardState extends State<_InboundJobCard> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final job = widget.job;
|
||||
final title = job.patientName?.trim().isNotEmpty == true
|
||||
? job.patientName!
|
||||
: job.patientCode;
|
||||
return Semantics(
|
||||
label: job.patientCode,
|
||||
label: title,
|
||||
button: true,
|
||||
excludeSemantics: true,
|
||||
child: Dismissible(
|
||||
@@ -246,9 +249,17 @@ class _InboundJobCardState extends State<_InboundJobCard> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
job.patientCode,
|
||||
title,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
if (job.patientName?.isNotEmpty == true) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
job.patientCode,
|
||||
style: TextStyle(
|
||||
color: AppColors.textMuted, fontSize: 12),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
job.clinicName ?? 'Klinik',
|
||||
@@ -259,7 +270,9 @@ class _InboundJobCardState extends State<_InboundJobCard> {
|
||||
Row(
|
||||
children: [
|
||||
_Chip(
|
||||
label: job.prostheticType.label,
|
||||
label: job.prostheticName?.isNotEmpty == true
|
||||
? '${job.prostheticType.label} · ${job.prostheticName}'
|
||||
: job.prostheticType.label,
|
||||
color: AppColors.inProgressBg,
|
||||
textColor: AppColors.inProgress,
|
||||
),
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import 'dart:async';
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
import '../../../core/api/pocketbase_client.dart';
|
||||
import '../../../core/services/finance_service.dart';
|
||||
import '../../../core/services/job_history_service.dart';
|
||||
import '../../../models/job.dart';
|
||||
|
||||
const _listExpand = 'clinic_tenant_id,lab_tenant_id';
|
||||
const _detailExpand = 'clinic_tenant_id,lab_tenant_id,patient_id';
|
||||
const _listExpand = 'clinic_tenant_id,lab_tenant_id,patient_id';
|
||||
const _detailExpand = 'clinic_tenant_id,lab_tenant_id,patient_id,prosthetic_id';
|
||||
|
||||
class LabJobsRepository {
|
||||
LabJobsRepository._();
|
||||
@@ -96,6 +97,7 @@ class LabJobsRepository {
|
||||
final record = await _pb.collection('jobs').update(jobId, body: {
|
||||
'status': 'cancelled',
|
||||
});
|
||||
await FinanceService.instance.deletePendingEntriesForJob(jobId);
|
||||
unawaited(JobHistoryService.instance.append(
|
||||
jobId: jobId,
|
||||
clinicTenantId: job.clinicTenantId,
|
||||
|
||||
Reference in New Issue
Block a user