Files
lab-app/lib/features/lab/jobs/lab_jobs_inbound_screen.dart
T
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

336 lines
10 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/providers/auth_provider.dart';
import '../../../core/services/realtime_service.dart';
import '../../../core/theme/app_theme.dart';
import '../../../core/widgets/gradient_app_bar.dart';
import '../../../models/job.dart';
import 'lab_jobs_repository.dart';
class LabJobsInboundScreen extends ConsumerStatefulWidget {
const LabJobsInboundScreen({super.key});
@override
ConsumerState<LabJobsInboundScreen> createState() =>
_LabJobsInboundScreenState();
}
class _LabJobsInboundScreenState extends ConsumerState<LabJobsInboundScreen> {
late Future<List<Job>> _future;
bool _acceptingAll = false;
late UnsubFn _unsub;
@override
void initState() {
super.initState();
_load();
final tenantId = ref.read(authProvider).activeTenant!.tenant.id;
_unsub = RealtimeService.instance.watch(
'jobs',
filter: "lab_tenant_id='$tenantId'",
onEvent: (_) { if (mounted) _load(); },
);
}
@override
void dispose() {
_unsub();
super.dispose();
}
void _load() {
final tenantId = ref.read(authProvider).activeTenant!.tenant.id;
setState(() {
_future =
LabJobsRepository.instance.listInbound(tenantId, status: 'pending');
});
}
Future<void> _acceptJob(Job job) async {
try {
await LabJobsRepository.instance.acceptJob(job);
_load();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('İş kabul edildi')),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Hata: $e')),
);
}
}
}
Future<void> _bulkAccept() async {
final tenantId = ref.read(authProvider).activeTenant!.tenant.id;
setState(() => _acceptingAll = true);
try {
await LabJobsRepository.instance.bulkAcceptPending(tenantId);
_load();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Tüm işler kabul edildi')),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Hata: $e')),
);
}
} finally {
if (mounted) setState(() => _acceptingAll = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.background,
appBar: GradientAppBar(
title: 'Gelen İşler',
category: 'LABORATUVAR',
),
floatingActionButton: FloatingActionButton.extended(
onPressed: _acceptingAll ? null : _bulkAccept,
icon: _acceptingAll
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2, color: Colors.white),
)
: const Icon(Icons.done_all),
label:
Text(_acceptingAll ? 'Kabul ediliyor...' : 'Tümünü Kabul Et'),
backgroundColor: AppColors.pending,
foregroundColor: Colors.white,
),
body: RefreshIndicator(
onRefresh: () async => _load(),
child: FutureBuilder<List<Job>>(
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'),
),
],
),
);
}
final jobs = snap.data!;
if (jobs.isEmpty) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.inbox_outlined, size: 64, color: AppColors.textMuted),
const SizedBox(height: 16),
Text(
'Bekleyen iş yok',
style: TextStyle(
fontSize: 16, color: AppColors.textSecondary),
),
],
),
);
}
return ListView.builder(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 80),
itemCount: jobs.length,
itemBuilder: (ctx, i) {
final job = jobs[i];
return _InboundJobCard(
job: job,
onAccept: () => _acceptJob(job),
);
},
);
},
),
),
);
}
}
class _InboundJobCard extends StatefulWidget {
const _InboundJobCard({required this.job, required this.onAccept});
final Job job;
final VoidCallback onAccept;
@override
State<_InboundJobCard> createState() => _InboundJobCardState();
}
class _InboundJobCardState extends State<_InboundJobCard> {
bool _accepting = false;
String _formatDate(DateTime dt) =>
'${dt.day.toString().padLeft(2, '0')}.${dt.month.toString().padLeft(2, '0')}.${dt.year}';
@override
Widget build(BuildContext context) {
final job = widget.job;
return Semantics(
label: job.patientCode,
button: true,
excludeSemantics: true,
child: Dismissible(
key: ValueKey(job.id),
direction: DismissDirection.endToStart,
background: Container(
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 20),
margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration(
color: AppColors.success,
borderRadius: BorderRadius.circular(12),
),
child: const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.check, color: Colors.white, size: 28),
SizedBox(height: 4),
Text('Kabul Et',
style: TextStyle(color: Colors.white, fontSize: 12)),
],
),
),
confirmDismiss: (_) async {
setState(() => _accepting = true);
try {
await LabJobsRepository.instance.acceptJob(job);
return true;
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Hata: $e')),
);
}
return false;
} finally {
if (mounted) setState(() => _accepting = false);
}
},
child: Card(
margin: const EdgeInsets.only(bottom: 8),
child: Padding(
padding: const EdgeInsets.all(14),
child: Row(
children: [
CircleAvatar(
backgroundColor: AppColors.pendingBg,
child: const Icon(Icons.assignment_outlined,
color: AppColors.pending),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
job.patientCode,
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 2),
Text(
job.clinicName ?? 'Klinik',
style: TextStyle(
color: AppColors.textSecondary, fontSize: 13),
),
const SizedBox(height: 4),
Row(
children: [
_Chip(
label: job.prostheticType.label,
color: AppColors.inProgressBg,
textColor: AppColors.inProgress,
),
const SizedBox(width: 6),
_Chip(
label: '${job.memberCount} üye',
color: AppColors.background,
textColor: AppColors.textSecondary,
),
],
),
const SizedBox(height: 4),
Text(
_formatDate(job.dateCreated),
style: TextStyle(
color: AppColors.textMuted, fontSize: 12),
),
],
),
),
const SizedBox(width: 8),
_accepting
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(strokeWidth: 2),
)
: FilledButton(
onPressed: widget.onAccept,
style: FilledButton.styleFrom(
backgroundColor: AppColors.success,
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 8),
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: const Text('Kabul Et',
style: TextStyle(fontSize: 13)),
),
],
),
),
),
),
);
}
}
class _Chip extends StatelessWidget {
const _Chip(
{required this.label,
required this.color,
required this.textColor});
final String label;
final Color color;
final Color textColor;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(10),
),
child: Text(label,
style: TextStyle(
color: textColor,
fontSize: 11,
fontWeight: FontWeight.w500)),
);
}
}