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
@@ -1,3 +1,4 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_animate/flutter_animate.dart';
@@ -9,6 +10,7 @@ import '../../../core/theme/app_theme.dart';
import '../../../core/widgets/tooth_logo.dart';
import '../../../core/services/realtime_service.dart';
import '../../../models/job.dart';
import '../../shared/location_completion_banner.dart';
import '../jobs/lab_jobs_repository.dart';
class LabDashboardScreen extends ConsumerStatefulWidget {
@@ -20,27 +22,47 @@ class LabDashboardScreen extends ConsumerStatefulWidget {
class _LabDashboardScreenState extends ConsumerState<LabDashboardScreen> {
late Future<_DashboardData> _future;
bool _acceptingAll = false;
late UnsubFn _unsub;
UnsubFn? _unsub;
Timer? _reloadDebounce;
String? _subscribedTenantId;
@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(); },
);
_ensureRealtimeSubscription();
}
@override
void dispose() {
_unsub();
_reloadDebounce?.cancel();
_unsub?.call();
super.dispose();
}
void _ensureRealtimeSubscription() {
final tenantId = ref.read(authProvider).activeTenant?.tenant.id;
if (tenantId == null || tenantId == _subscribedTenantId) return;
_unsub?.call();
_subscribedTenantId = tenantId;
_unsub = RealtimeService.instance.watch(
'jobs',
filter: "lab_tenant_id='$tenantId'",
onEvent: (_) {
_scheduleReload();
},
);
}
void _scheduleReload() {
_reloadDebounce?.cancel();
_reloadDebounce = Timer(const Duration(milliseconds: 250), () {
if (mounted) _load();
});
}
void _load() {
_ensureRealtimeSubscription();
final tenantId = ref.read(authProvider).activeTenant!.tenant.id;
final now = DateTime.now();
final thisMonthStart = DateTime(now.year, now.month, 1);
@@ -50,13 +72,19 @@ class _LabDashboardScreenState extends ConsumerState<LabDashboardScreen> {
Future.wait<List<Job>>([
LabJobsRepository.instance.listInbound(tenantId, status: 'pending'),
LabJobsRepository.instance.listInProgress(tenantId),
LabJobsRepository.instance.listInProgress(tenantId, location: 'at_lab'),
LabJobsRepository.instance.listInProgress(tenantId, location: 'at_clinic'),
LabJobsRepository.instance.listInbound(tenantId, status: 'sent', limit: 200),
LabJobsRepository.instance.listInbound(tenantId, status: 'delivered', limit: 200),
LabJobsRepository.instance
.listInProgress(tenantId, location: 'at_lab'),
LabJobsRepository.instance
.listInProgress(tenantId, location: 'at_clinic'),
LabJobsRepository.instance
.listInbound(tenantId, status: 'sent', limit: 200),
LabJobsRepository.instance
.listInbound(tenantId, status: 'delivered', limit: 200),
]),
LabJobsRepository.instance.countDelivered(tenantId, from: thisMonthStart),
LabJobsRepository.instance.countDelivered(tenantId, from: lastMonthStart, to: thisMonthStart),
LabJobsRepository.instance
.countDelivered(tenantId, from: thisMonthStart),
LabJobsRepository.instance
.countDelivered(tenantId, from: lastMonthStart, to: thisMonthStart),
]).then((r) {
final jobs = r[0] as List<List<Job>>;
return _DashboardData(
@@ -82,7 +110,8 @@ class _LabDashboardScreenState extends ConsumerState<LabDashboardScreen> {
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Hata: $e'), behavior: SnackBarBehavior.floating),
SnackBar(
content: Text('Hata: $e'), behavior: SnackBarBehavior.floating),
);
}
} finally {
@@ -92,7 +121,10 @@ class _LabDashboardScreenState extends ConsumerState<LabDashboardScreen> {
@override
Widget build(BuildContext context) {
final companyName = ref.watch(authProvider).activeTenant?.tenant.companyName ?? '';
_ensureRealtimeSubscription();
final activeTenant = ref.watch(authProvider).activeTenant?.tenant;
final companyName = activeTenant?.companyName ?? '';
final showLocationWarning = activeTenant?.hasLocation != true;
return Scaffold(
backgroundColor: AppColors.background,
body: LayoutBuilder(
@@ -109,14 +141,29 @@ class _LabDashboardScreenState extends ConsumerState<LabDashboardScreen> {
future: _future,
builder: (ctx, snap) {
if (snap.connectionState == ConnectionState.waiting) {
return _DashboardSkeleton(companyName: companyName, hPad: hPad);
return _DashboardSkeleton(
companyName: companyName, hPad: hPad);
}
if (snap.hasError) return _ErrorBody(onRetry: _load);
final data = snap.data!;
final isDesktop = MediaQuery.sizeOf(ctx).width > AppLayout.sidebarBreakpoint;
final isDesktop =
MediaQuery.sizeOf(ctx).width > AppLayout.sidebarBreakpoint;
return CustomScrollView(
slivers: [
_DashboardHeader(companyName: companyName),
if (showLocationWarning)
SliverPadding(
padding: EdgeInsets.fromLTRB(hPad, 16, hPad, 0),
sliver: SliverToBoxAdapter(
child: LocationCompletionBanner(
title: 'Konum kaydı eksik',
description:
'Haritada görünmek ve kliniklerin sizi yakın laboratuvar olarak bulabilmesi için konumunuzu tamamlayın.',
buttonLabel: 'Konumu Tamamla',
onTap: () => context.go(routeLabSettings),
),
),
),
if (isDesktop)
SliverPadding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
@@ -137,7 +184,10 @@ class _LabDashboardScreenState extends ConsumerState<LabDashboardScreen> {
count: data.pendingJobs.length,
loading: _acceptingAll,
onTap: _bulkAccept,
).animate().fadeIn(duration: 300.ms).slideY(begin: 0.1, end: 0),
)
.animate()
.fadeIn(duration: 300.ms)
.slideY(begin: 0.1, end: 0),
),
),
if (isDesktop) ...[
@@ -145,14 +195,18 @@ class _LabDashboardScreenState extends ConsumerState<LabDashboardScreen> {
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
sliver: SliverToBoxAdapter(
child: _MonthlyReportSection(data: data)
.animate().fadeIn(duration: 300.ms).slideY(begin: 0.08, end: 0),
.animate()
.fadeIn(duration: 300.ms)
.slideY(begin: 0.08, end: 0),
),
),
SliverPadding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
sliver: SliverToBoxAdapter(
child: _GamificationRow(data: data)
.animate().fadeIn(duration: 300.ms, delay: 60.ms).slideY(begin: 0.08, end: 0),
.animate()
.fadeIn(duration: 300.ms, delay: 60.ms)
.slideY(begin: 0.08, end: 0),
),
),
],
@@ -163,10 +217,14 @@ class _LabDashboardScreenState extends ConsumerState<LabDashboardScreen> {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Yapılacaklar', style: Theme.of(context).textTheme.titleMedium),
Text('Yapılacaklar',
style: Theme.of(context).textTheme.titleMedium),
TextButton(
onPressed: () => context.go(routeLabJobsAll),
style: TextButton.styleFrom(foregroundColor: AppColors.accent, padding: const EdgeInsets.symmetric(horizontal: 8)),
style: TextButton.styleFrom(
foregroundColor: AppColors.accent,
padding: const EdgeInsets.symmetric(
horizontal: 8)),
child: const Text('Tümünü Gör'),
),
],
@@ -185,11 +243,13 @@ class _LabDashboardScreenState extends ConsumerState<LabDashboardScreen> {
padding: const EdgeInsets.fromLTRB(16, 0, 16, 0),
sliver: SliverList.separated(
itemCount: data.atLabJobs.take(5).length,
separatorBuilder: (_, __) => const SizedBox(height: 10),
itemBuilder: (ctx, i) => _JobCard(job: data.atLabJobs[i])
.animate(delay: (i * 60).ms)
.fadeIn(duration: 300.ms)
.slideY(begin: 0.12, end: 0),
separatorBuilder: (_, __) =>
const SizedBox(height: 10),
itemBuilder: (ctx, i) =>
_JobCard(job: data.atLabJobs[i])
.animate(delay: (i * 60).ms)
.fadeIn(duration: 300.ms)
.slideY(begin: 0.12, end: 0),
),
),
// ── Klinikte Onay Bekliyor (at_clinic) ───────────────
@@ -197,18 +257,21 @@ class _LabDashboardScreenState extends ConsumerState<LabDashboardScreen> {
SliverPadding(
padding: const EdgeInsets.fromLTRB(16, 20, 16, 4),
sliver: SliverToBoxAdapter(
child: Text('Klinikte Onay Bekliyor', style: Theme.of(context).textTheme.titleMedium),
child: Text('Klinikte Onay Bekliyor',
style: Theme.of(context).textTheme.titleMedium),
),
),
SliverPadding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 0),
sliver: SliverList.separated(
itemCount: data.atClinicJobs.take(5).length,
separatorBuilder: (_, __) => const SizedBox(height: 10),
itemBuilder: (ctx, i) => _JobCard(job: data.atClinicJobs[i])
.animate(delay: (i * 60).ms)
.fadeIn(duration: 300.ms)
.slideY(begin: 0.12, end: 0),
separatorBuilder: (_, __) =>
const SizedBox(height: 10),
itemBuilder: (ctx, i) =>
_JobCard(job: data.atClinicJobs[i])
.animate(delay: (i * 60).ms)
.fadeIn(duration: 300.ms)
.slideY(begin: 0.12, end: 0),
),
),
],
@@ -233,7 +296,8 @@ class _DashboardHeader extends StatelessWidget {
@override
Widget build(BuildContext context) {
final isDesktop = MediaQuery.sizeOf(context).width > AppLayout.sidebarBreakpoint;
final isDesktop =
MediaQuery.sizeOf(context).width > AppLayout.sidebarBreakpoint;
if (isDesktop) {
return SliverAppBar(
@@ -252,15 +316,24 @@ class _DashboardHeader extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text('Genel Bakış', style: TextStyle(fontSize: 11, color: AppColors.textSecondary.withValues(alpha: 0.8), letterSpacing: 0.3)),
const Text('Bugünkü Durum', style: TextStyle(fontSize: 17, fontWeight: FontWeight.w700, color: AppColors.textPrimary)),
Text('Genel Bakış',
style: TextStyle(
fontSize: 11,
color: AppColors.textSecondary.withValues(alpha: 0.8),
letterSpacing: 0.3)),
const Text('Bugünkü Durum',
style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.w700,
color: AppColors.textPrimary)),
],
),
),
actions: [
IconButton(
onPressed: () => context.go(routeLabSettings),
icon: const Icon(Icons.settings_outlined, color: AppColors.textSecondary, size: 22),
icon: const Icon(Icons.settings_outlined,
color: AppColors.textSecondary, size: 22),
),
const SizedBox(width: 8),
],
@@ -291,14 +364,26 @@ class _DashboardHeader extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text('DLS', style: TextStyle(color: Colors.white.withValues(alpha: 0.65), fontSize: 11, fontWeight: FontWeight.w600, letterSpacing: 1.5)),
Text(companyName, style: const TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.w700), maxLines: 1, overflow: TextOverflow.ellipsis),
Text('DLS',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.65),
fontSize: 11,
fontWeight: FontWeight.w600,
letterSpacing: 1.5)),
Text(companyName,
style: const TextStyle(
color: Colors.white,
fontSize: 15,
fontWeight: FontWeight.w700),
maxLines: 1,
overflow: TextOverflow.ellipsis),
],
),
actions: [
IconButton(
onPressed: () => context.go(routeLabSettings),
icon: const Icon(Icons.settings_outlined, color: Colors.white, size: 22),
icon: const Icon(Icons.settings_outlined,
color: Colors.white, size: 22),
),
],
flexibleSpace: FlexibleSpaceBar(
@@ -317,9 +402,19 @@ class _DashboardHeader extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text('Genel Bakış', style: TextStyle(color: Colors.white.withValues(alpha: 0.65), fontSize: 12, fontWeight: FontWeight.w500, letterSpacing: 0.5)),
Text('Genel Bakış',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.65),
fontSize: 12,
fontWeight: FontWeight.w500,
letterSpacing: 0.5)),
const SizedBox(height: 4),
const Text('Bugünkü Durum', style: TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.w800, letterSpacing: -0.5)),
const Text('Bugünkü Durum',
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.w800,
letterSpacing: -0.5)),
],
),
),
@@ -343,18 +438,47 @@ class _StatsRow extends StatelessWidget {
@override
Widget build(BuildContext context) {
final isWideDesktop = MediaQuery.sizeOf(context).width >= AppLayout.wideBreakpoint;
final isWideDesktop =
MediaQuery.sizeOf(context).width >= AppLayout.wideBreakpoint;
final pendingCard = _StatCard(label: 'Bekleyen', value: '$pending', icon: Icons.hourglass_top_rounded, color: AppColors.pending, bgColor: AppColors.pendingBg)
.animate().fadeIn(duration: 350.ms).slideY(begin: 0.2, end: 0);
final inProgressCard = _StatCard(label: 'Devam Eden', value: '$inProgress', icon: Icons.autorenew_rounded, color: AppColors.inProgress, bgColor: AppColors.inProgressBg)
.animate(delay: 80.ms).fadeIn(duration: 350.ms).slideY(begin: 0.2, end: 0);
final pendingCard = _StatCard(
label: 'Bekleyen',
value: '$pending',
icon: Icons.hourglass_top_rounded,
color: AppColors.pending,
bgColor: AppColors.pendingBg)
.animate()
.fadeIn(duration: 350.ms)
.slideY(begin: 0.2, end: 0);
final inProgressCard = _StatCard(
label: 'Devam Eden',
value: '$inProgress',
icon: Icons.autorenew_rounded,
color: AppColors.inProgress,
bgColor: AppColors.inProgressBg)
.animate(delay: 80.ms)
.fadeIn(duration: 350.ms)
.slideY(begin: 0.2, end: 0);
if (isWideDesktop) {
final sentCard = _StatCard(label: 'Klinik\'te', value: '$sent', icon: Icons.local_hospital_outlined, color: AppColors.accent, bgColor: AppColors.inProgressBg)
.animate(delay: 120.ms).fadeIn(duration: 350.ms).slideY(begin: 0.2, end: 0);
final deliveredCard = _StatCard(label: 'Tamamlanan', value: '$delivered', icon: Icons.task_alt, color: AppColors.success, bgColor: AppColors.successBg)
.animate(delay: 160.ms).fadeIn(duration: 350.ms).slideY(begin: 0.2, end: 0);
final sentCard = _StatCard(
label: 'Klinik\'te',
value: '$sent',
icon: Icons.local_hospital_outlined,
color: AppColors.accent,
bgColor: AppColors.inProgressBg)
.animate(delay: 120.ms)
.fadeIn(duration: 350.ms)
.slideY(begin: 0.2, end: 0);
final deliveredCard = _StatCard(
label: 'Tamamlanan',
value: '$delivered',
icon: Icons.task_alt,
color: AppColors.success,
bgColor: AppColors.successBg)
.animate(delay: 160.ms)
.fadeIn(duration: 350.ms)
.slideY(begin: 0.2, end: 0);
return Row(
children: [
@@ -380,7 +504,12 @@ class _StatsRow extends StatelessWidget {
}
class _StatCard extends StatelessWidget {
const _StatCard({required this.label, required this.value, required this.icon, required this.color, required this.bgColor});
const _StatCard(
{required this.label,
required this.value,
required this.icon,
required this.color,
required this.bgColor});
final String label;
final String value;
final IconData icon;
@@ -394,22 +523,38 @@ class _StatCard extends StatelessWidget {
color: AppColors.surface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColors.border),
boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.06), blurRadius: 12, offset: const Offset(0, 4))],
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.06),
blurRadius: 12,
offset: const Offset(0, 4))
],
),
child: Row(
children: [
Container(
width: 44, height: 44,
decoration: BoxDecoration(color: bgColor, borderRadius: BorderRadius.circular(12)),
width: 44,
height: 44,
decoration: BoxDecoration(
color: bgColor, borderRadius: BorderRadius.circular(12)),
child: Icon(icon, color: color, size: 22),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(value, style: TextStyle(fontSize: 28, fontWeight: FontWeight.w800, color: color, height: 1)),
Text(value,
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.w800,
color: color,
height: 1)),
const SizedBox(height: 3),
Text(label, style: const TextStyle(fontSize: 12, color: AppColors.textSecondary, fontWeight: FontWeight.w500)),
Text(label,
style: const TextStyle(
fontSize: 12,
color: AppColors.textSecondary,
fontWeight: FontWeight.w500)),
],
),
],
@@ -419,7 +564,8 @@ class _StatCard extends StatelessWidget {
}
class _AcceptAllBanner extends StatelessWidget {
const _AcceptAllBanner({required this.count, required this.loading, required this.onTap});
const _AcceptAllBanner(
{required this.count, required this.loading, required this.onTap});
final int count;
final bool loading;
final VoidCallback onTap;
@@ -433,30 +579,54 @@ class _AcceptAllBanner extends StatelessWidget {
borderRadius: BorderRadius.circular(14),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(14), border: Border.all(color: AppColors.pending.withValues(alpha: 0.35))),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(14),
border:
Border.all(color: AppColors.pending.withValues(alpha: 0.35))),
child: Row(
children: [
Container(
width: 38, height: 38,
decoration: BoxDecoration(color: AppColors.pending.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(10)),
child: const Icon(Icons.notifications_active_outlined, color: AppColors.pending, size: 18),
width: 38,
height: 38,
decoration: BoxDecoration(
color: AppColors.pending.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(10)),
child: const Icon(Icons.notifications_active_outlined,
color: AppColors.pending, size: 18),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('$count yeni iş bekliyor', style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.textPrimary)),
const Text('Tümünü hızlıca kabul et', style: TextStyle(fontSize: 12, color: AppColors.textSecondary)),
Text('$count yeni iş bekliyor',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary)),
const Text('Tümünü hızlıca kabul et',
style: TextStyle(
fontSize: 12, color: AppColors.textSecondary)),
],
),
),
loading
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: AppColors.pending))
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2, color: AppColors.pending))
: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(color: AppColors.pending, borderRadius: BorderRadius.circular(8)),
child: const Text('Kabul Et', style: TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.w600)),
padding: const EdgeInsets.symmetric(
horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: AppColors.pending,
borderRadius: BorderRadius.circular(8)),
child: const Text('Kabul Et',
style: TextStyle(
color: Colors.white,
fontSize: 13,
fontWeight: FontWeight.w600)),
),
],
),
@@ -473,62 +643,97 @@ class _JobCard extends StatelessWidget {
Widget build(BuildContext context) {
final due = job.dueDate;
final isOverdue = due != null && due.isBefore(DateTime.now());
final dueText = due != null ? '${due.day.toString().padLeft(2, '0')}.${due.month.toString().padLeft(2, '0')}.${due.year}' : null;
final dueText = due != null
? '${due.day.toString().padLeft(2, '0')}.${due.month.toString().padLeft(2, '0')}.${due.year}'
: null;
return Semantics(
label: job.patientCode,
button: true,
excludeSemantics: true,
child: Material(
color: AppColors.surface,
borderRadius: BorderRadius.circular(14),
child: InkWell(
onTap: () => context.push('/lab/jobs/${job.id}'),
color: AppColors.surface,
borderRadius: BorderRadius.circular(14),
child: Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(14), border: Border.all(color: AppColors.border)),
child: Row(
children: [
Container(
width: 46, height: 46,
decoration: BoxDecoration(color: AppColors.inProgressBg, borderRadius: BorderRadius.circular(12)),
child: Center(child: Text('${job.memberCount}', style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w800, color: AppColors.inProgress))),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(job.patientCode, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: AppColors.textPrimary)),
const SizedBox(height: 2),
Text(job.clinicName ?? 'Klinik', style: const TextStyle(fontSize: 13, color: AppColors.textSecondary)),
const SizedBox(height: 6),
Wrap(
spacing: 6,
children: [
_Tag(label: job.prostheticType.label, color: AppColors.inProgress, bg: AppColors.inProgressBg),
if (job.currentStep != null) _Tag(label: job.currentStep!.label, color: AppColors.success, bg: AppColors.successBg),
],
),
],
child: InkWell(
onTap: () => context.push('/lab/jobs/${job.id}'),
borderRadius: BorderRadius.circular(14),
child: Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(14),
border: Border.all(color: AppColors.border)),
child: Row(
children: [
Container(
width: 46,
height: 46,
decoration: BoxDecoration(
color: AppColors.inProgressBg,
borderRadius: BorderRadius.circular(12)),
child: Center(
child: Text('${job.memberCount}',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.w800,
color: AppColors.inProgress))),
),
),
if (dueText != null) ...[
const SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Icon(Icons.calendar_today_outlined, size: 13, color: isOverdue ? AppColors.cancelled : AppColors.textMuted),
const SizedBox(height: 3),
Text(dueText, style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500, color: isOverdue ? AppColors.cancelled : AppColors.textSecondary)),
],
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(job.patientCode,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary)),
const SizedBox(height: 2),
Text(job.clinicName ?? 'Klinik',
style: const TextStyle(
fontSize: 13, color: AppColors.textSecondary)),
const SizedBox(height: 6),
Wrap(
spacing: 6,
children: [
_Tag(
label: job.prostheticType.label,
color: AppColors.inProgress,
bg: AppColors.inProgressBg),
if (job.currentStep != null)
_Tag(
label: job.currentStep!.label,
color: AppColors.success,
bg: AppColors.successBg),
],
),
],
),
),
if (dueText != null) ...[
const SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Icon(Icons.calendar_today_outlined,
size: 13,
color: isOverdue
? AppColors.cancelled
: AppColors.textMuted),
const SizedBox(height: 3),
Text(dueText,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: isOverdue
? AppColors.cancelled
: AppColors.textSecondary)),
],
),
],
],
],
),
),
),
),
),
);
}
}
@@ -542,13 +747,15 @@ class _Tag extends StatelessWidget {
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(color: bg, borderRadius: BorderRadius.circular(6)),
child: Text(label, style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: color)),
decoration:
BoxDecoration(color: bg, borderRadius: BorderRadius.circular(6)),
child: Text(label,
style: TextStyle(
fontSize: 11, fontWeight: FontWeight.w600, color: color)),
);
}
}
class _EmptySection extends StatelessWidget {
const _EmptySection({required this.message});
final String message;
@@ -563,9 +770,12 @@ class _EmptySection extends StatelessWidget {
),
child: Row(
children: [
Icon(Icons.check_circle_outline_rounded, color: AppColors.textSecondary.withValues(alpha: 0.5), size: 20),
Icon(Icons.check_circle_outline_rounded,
color: AppColors.textSecondary.withValues(alpha: 0.5), size: 20),
const SizedBox(width: 10),
Text(message, style: TextStyle(fontSize: 14, color: AppColors.textSecondary)),
Text(message,
style: const TextStyle(
fontSize: 14, color: AppColors.textSecondary)),
],
),
);
@@ -584,14 +794,25 @@ class _ErrorBody extends StatelessWidget {
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),
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),
const Text('Bağlantı hatası', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.textPrimary)),
const Text('Bağlantı hatası',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary)),
const SizedBox(height: 12),
FilledButton.icon(onPressed: onRetry, icon: const Icon(Icons.refresh_rounded, size: 18), label: const Text('Tekrar Dene')),
FilledButton.icon(
onPressed: onRetry,
icon: const Icon(Icons.refresh_rounded, size: 18),
label: const Text('Tekrar Dene')),
],
),
),
@@ -623,7 +844,9 @@ class _DashboardSkeleton extends StatelessWidget {
padding: EdgeInsets.fromLTRB(hPad, 8, hPad, 0),
sliver: SliverList.builder(
itemCount: 4,
itemBuilder: (_, i) => const Padding(padding: EdgeInsets.only(bottom: 10), child: _ShimmerBox(height: 92, radius: 14)),
itemBuilder: (_, i) => const Padding(
padding: EdgeInsets.only(bottom: 10),
child: _ShimmerBox(height: 92, radius: 14)),
),
),
],
@@ -639,24 +862,35 @@ class _ShimmerBox extends StatefulWidget {
State<_ShimmerBox> createState() => _ShimmerBoxState();
}
class _ShimmerBoxState extends State<_ShimmerBox> with SingleTickerProviderStateMixin {
class _ShimmerBoxState extends State<_ShimmerBox>
with SingleTickerProviderStateMixin {
late AnimationController _ctrl;
late Animation<double> _anim;
@override
void initState() {
super.initState();
_ctrl = AnimationController(vsync: this, duration: const Duration(milliseconds: 1100))..repeat(reverse: true);
_ctrl = AnimationController(
vsync: this, duration: const Duration(milliseconds: 1100))
..repeat(reverse: true);
_anim = CurvedAnimation(parent: _ctrl, curve: Curves.easeInOut);
}
@override
void dispose() { _ctrl.dispose(); super.dispose(); }
void dispose() {
_ctrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _anim,
builder: (_, __) => Container(
height: widget.height,
decoration: BoxDecoration(borderRadius: BorderRadius.circular(widget.radius), color: Color.lerp(const Color(0xFFE2E8F0), const Color(0xFFF1F5F9), _anim.value)),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(widget.radius),
color: Color.lerp(
const Color(0xFFE2E8F0), const Color(0xFFF1F5F9), _anim.value)),
),
);
}
@@ -685,9 +919,14 @@ class _MonthlyReportSection extends StatelessWidget {
children: [
Row(
children: [
const Icon(Icons.bar_chart_rounded, size: 18, color: AppColors.accent),
const Icon(Icons.bar_chart_rounded,
size: 18, color: AppColors.accent),
const SizedBox(width: 6),
Text('Aylık Rapor', style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600)),
Text('Aylık Rapor',
style: Theme.of(context)
.textTheme
.titleSmall
?.copyWith(fontWeight: FontWeight.w600)),
],
),
const SizedBox(height: 12),
@@ -710,7 +949,8 @@ class _MonthlyReportSection extends StatelessWidget {
),
const SizedBox(width: 12),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
padding:
const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: isUp ? AppColors.successBg : AppColors.cancelledBg,
borderRadius: BorderRadius.circular(8),
@@ -719,7 +959,9 @@ class _MonthlyReportSection extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
Icon(
isUp ? Icons.trending_up_rounded : Icons.trending_down_rounded,
isUp
? Icons.trending_up_rounded
: Icons.trending_down_rounded,
size: 16,
color: isUp ? AppColors.success : AppColors.cancelled,
),
@@ -744,7 +986,8 @@ class _MonthlyReportSection extends StatelessWidget {
}
class _MonthStat extends StatelessWidget {
const _MonthStat({required this.label, required this.value, required this.highlighted});
const _MonthStat(
{required this.label, required this.value, required this.highlighted});
final String label;
final int value;
final bool highlighted;
@@ -754,14 +997,22 @@ class _MonthStat extends StatelessWidget {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
color: highlighted ? AppColors.accent.withValues(alpha: 0.06) : AppColors.background,
color: highlighted
? AppColors.accent.withValues(alpha: 0.06)
: AppColors.background,
borderRadius: BorderRadius.circular(8),
border: highlighted ? Border.all(color: AppColors.accent.withValues(alpha: 0.2)) : null,
border: highlighted
? Border.all(color: AppColors.accent.withValues(alpha: 0.2))
: null,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: TextStyle(fontSize: 11, color: AppColors.textSecondary, fontWeight: FontWeight.w500)),
Text(label,
style: const TextStyle(
fontSize: 11,
color: AppColors.textSecondary,
fontWeight: FontWeight.w500)),
const SizedBox(height: 2),
Text(
'$value',
@@ -788,7 +1039,8 @@ class _GamificationRow extends StatelessWidget {
@override
Widget build(BuildContext context) {
final progress = (data.thisMonthDelivered / _monthlyGoal).clamp(0.0, 1.0);
final remaining = (_monthlyGoal - data.thisMonthDelivered).clamp(0, _monthlyGoal);
final remaining =
(_monthlyGoal - data.thisMonthDelivered).clamp(0, _monthlyGoal);
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
@@ -803,7 +1055,11 @@ class _GamificationRow extends StatelessWidget {
children: [
const Text('🏆', style: TextStyle(fontSize: 16)),
const SizedBox(width: 6),
Text('Aylık Hedef', style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600)),
Text('Aylık Hedef',
style: Theme.of(context)
.textTheme
.titleSmall
?.copyWith(fontWeight: FontWeight.w600)),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
@@ -813,7 +1069,10 @@ class _GamificationRow extends StatelessWidget {
),
child: Text(
'${data.points} puan',
style: const TextStyle(fontSize: 11, fontWeight: FontWeight.w700, color: AppColors.primary),
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
color: AppColors.primary),
),
),
],
@@ -836,14 +1095,17 @@ class _GamificationRow extends StatelessWidget {
children: [
Text(
'${data.thisMonthDelivered} / $_monthlyGoal iş teslim edildi',
style: TextStyle(fontSize: 12, color: AppColors.textSecondary),
style: const TextStyle(
fontSize: 12, color: AppColors.textSecondary),
),
Text(
progress >= 1.0 ? 'Hedef tamamlandı!' : '$remaining iş kaldı',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: progress >= 1.0 ? AppColors.success : AppColors.textSecondary,
color: progress >= 1.0
? AppColors.success
: AppColors.textSecondary,
),
),
],