import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../../core/providers/auth_provider.dart'; import '../../../core/router/app_router.dart'; import '../../../core/theme/app_theme.dart'; import '../../../core/widgets/tooth_logo.dart'; import '../../../core/services/realtime_service.dart'; import '../../../models/job.dart'; import '../jobs/lab_jobs_repository.dart'; class LabDashboardScreen extends ConsumerStatefulWidget { const LabDashboardScreen({super.key}); @override ConsumerState createState() => _LabDashboardScreenState(); } class _LabDashboardScreenState extends ConsumerState { late Future<_DashboardData> _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; final now = DateTime.now(); final thisMonthStart = DateTime(now.year, now.month, 1); final lastMonthStart = DateTime(now.year, now.month - 1, 1); setState(() { _future = Future.wait([ Future.wait>([ 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.countDelivered(tenantId, from: thisMonthStart), LabJobsRepository.instance.countDelivered(tenantId, from: lastMonthStart, to: thisMonthStart), ]).then((r) { final jobs = r[0] as List>; return _DashboardData( pendingJobs: jobs[0], inProgressJobs: jobs[1], atLabJobs: jobs[2], atClinicJobs: jobs[3], sentCount: jobs[4].length, deliveredCount: jobs[5].length, thisMonthDelivered: r[1] as int, lastMonthDelivered: r[2] as int, ); }); }); } Future _bulkAccept() async { final tenantId = ref.read(authProvider).activeTenant!.tenant.id; setState(() => _acceptingAll = true); try { await LabJobsRepository.instance.bulkAcceptPending(tenantId); _load(); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Hata: $e'), behavior: SnackBarBehavior.floating), ); } } finally { if (mounted) setState(() => _acceptingAll = false); } } @override Widget build(BuildContext context) { final companyName = ref.watch(authProvider).activeTenant?.tenant.companyName ?? ''; return Scaffold( backgroundColor: AppColors.background, body: LayoutBuilder( builder: (context, constraints) { const maxContent = 1040.0; final hPad = constraints.maxWidth > maxContent ? (constraints.maxWidth - maxContent) / 2 : 16.0; return RefreshIndicator( color: AppColors.accent, onRefresh: () async => _load(), child: FutureBuilder<_DashboardData>( future: _future, builder: (ctx, snap) { if (snap.connectionState == ConnectionState.waiting) { 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; return CustomScrollView( slivers: [ _DashboardHeader(companyName: companyName), if (isDesktop) SliverPadding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), sliver: SliverToBoxAdapter( child: _StatsRow( pending: data.pendingJobs.length, inProgress: data.inProgressJobs.length, sent: data.sentCount, delivered: data.deliveredCount, ), ), ), if (data.pendingJobs.isNotEmpty) SliverPadding( padding: const EdgeInsets.fromLTRB(16, 4, 16, 0), sliver: SliverToBoxAdapter( child: _AcceptAllBanner( count: data.pendingJobs.length, loading: _acceptingAll, onTap: _bulkAccept, ).animate().fadeIn(duration: 300.ms).slideY(begin: 0.1, end: 0), ), ), if (isDesktop) ...[ SliverPadding( 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), ), ), 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), ), ), ], // ── Yapılacaklar (at_lab) ──────────────────────────── SliverPadding( padding: const EdgeInsets.fromLTRB(16, 20, 16, 4), sliver: SliverToBoxAdapter( child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ 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)), child: const Text('Tümünü Gör'), ), ], ), ), ), if (data.atLabJobs.isEmpty) const SliverToBoxAdapter( child: Padding( padding: EdgeInsets.fromLTRB(16, 4, 16, 0), child: _EmptySection(message: 'Yapılacak iş yok'), ), ) else SliverPadding( 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), ), ), // ── Klinikte Onay Bekliyor (at_clinic) ─────────────── if (data.atClinicJobs.isNotEmpty) ...[ SliverPadding( padding: const EdgeInsets.fromLTRB(16, 20, 16, 4), sliver: SliverToBoxAdapter( 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), ), ), ], const SliverToBoxAdapter(child: SizedBox(height: 24)), ], ); }, ), ); }, ), ); } } class _DashboardHeader extends StatelessWidget { const _DashboardHeader({required this.companyName}); final String companyName; // Must stay in sync with _DesktopSidebar.headerHeight in app_router.dart static const double _desktopToolbarHeight = 64; @override Widget build(BuildContext context) { final isDesktop = MediaQuery.sizeOf(context).width > AppLayout.sidebarBreakpoint; if (isDesktop) { return SliverAppBar( pinned: true, toolbarHeight: _desktopToolbarHeight, backgroundColor: AppColors.surface, surfaceTintColor: Colors.transparent, elevation: 0, scrolledUnderElevation: 0, centerTitle: false, automaticallyImplyLeading: false, titleSpacing: 0, title: Padding( padding: const EdgeInsets.symmetric(horizontal: 24), child: Column( 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)), ], ), ), actions: [ IconButton( onPressed: () => context.go(routeLabSettings), icon: const Icon(Icons.settings_outlined, color: AppColors.textSecondary, size: 22), ), const SizedBox(width: 8), ], ); } return SliverAppBar( pinned: true, expandedHeight: 148, backgroundColor: AppColors.primary, surfaceTintColor: Colors.transparent, shadowColor: Colors.transparent, systemOverlayStyle: SystemUiOverlayStyle.light, centerTitle: false, leadingWidth: 60, leading: Padding( padding: const EdgeInsets.only(left: 16, top: 8, bottom: 8), child: Container( decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(10), ), child: const Center(child: ToothLogo(size: 20, color: Colors.white)), ), ), titleSpacing: 8, title: Column( 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), ], ), actions: [ IconButton( onPressed: () => context.go(routeLabSettings), icon: const Icon(Icons.settings_outlined, color: Colors.white, size: 22), ), ], flexibleSpace: FlexibleSpaceBar( collapseMode: CollapseMode.pin, background: Container( decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [AppColors.primary, AppColors.accent], ), ), child: Padding( padding: const EdgeInsets.fromLTRB(20, 0, 20, 24), child: Column( 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)), const SizedBox(height: 4), const Text('Bugünkü Durum', style: TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.w800, letterSpacing: -0.5)), ], ), ), ), ), ); } } class _StatsRow extends StatelessWidget { const _StatsRow({ required this.pending, required this.inProgress, required this.sent, required this.delivered, }); final int pending; final int inProgress; final int sent; final int delivered; @override Widget build(BuildContext context) { 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); 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); return Row( children: [ Expanded(child: pendingCard), const SizedBox(width: 12), Expanded(child: inProgressCard), const SizedBox(width: 12), Expanded(child: sentCard), const SizedBox(width: 12), Expanded(child: deliveredCard), ], ); } return Row( children: [ Expanded(child: pendingCard), const SizedBox(width: 12), Expanded(child: inProgressCard), ], ); } } class _StatCard extends StatelessWidget { 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; final Color color; final Color bgColor; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( 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))], ), child: Row( children: [ Container( 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)), const SizedBox(height: 3), Text(label, style: const TextStyle(fontSize: 12, color: AppColors.textSecondary, fontWeight: FontWeight.w500)), ], ), ], ), ); } } class _AcceptAllBanner extends StatelessWidget { const _AcceptAllBanner({required this.count, required this.loading, required this.onTap}); final int count; final bool loading; final VoidCallback onTap; @override Widget build(BuildContext context) { return Material( color: AppColors.pendingBg, borderRadius: BorderRadius.circular(14), child: InkWell( onTap: loading ? null : onTap, 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))), 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), ), 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)), ], ), ), loading ? 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)), ), ], ), ), ), ); } } class _JobCard extends StatelessWidget { const _JobCard({required this.job}); final Job job; @override 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; 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}'), 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), ], ), ], ), ), 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)), ], ), ], ], ), ), ), ), ); } } class _Tag extends StatelessWidget { const _Tag({required this.label, required this.color, required this.bg}); final String label; final Color color; final Color bg; @override 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)), ); } } class _EmptySection extends StatelessWidget { const _EmptySection({required this.message}); final String message; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.border), ), child: Row( children: [ 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)), ], ), ); } } class _ErrorBody extends StatelessWidget { const _ErrorBody({required this.onRetry}); final VoidCallback onRetry; @override Widget build(BuildContext context) { return Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( 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), ), const SizedBox(height: 16), 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')), ], ), ), ); } } class _DashboardSkeleton extends StatelessWidget { const _DashboardSkeleton({required this.companyName, required this.hPad}); final String companyName; final double hPad; @override Widget build(BuildContext context) { return CustomScrollView( physics: const NeverScrollableScrollPhysics(), slivers: [ _DashboardHeader(companyName: companyName), SliverPadding( padding: EdgeInsets.fromLTRB(hPad, 16, hPad, 0), sliver: const SliverToBoxAdapter( child: Row(children: [ Expanded(child: _ShimmerBox(height: 84, radius: 16)), SizedBox(width: 12), Expanded(child: _ShimmerBox(height: 84, radius: 16)), ]), ), ), SliverPadding( 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)), ), ), ], ); } } class _ShimmerBox extends StatefulWidget { const _ShimmerBox({required this.height, required this.radius}); final double height; final double radius; @override State<_ShimmerBox> createState() => _ShimmerBoxState(); } class _ShimmerBoxState extends State<_ShimmerBox> with SingleTickerProviderStateMixin { late AnimationController _ctrl; late Animation _anim; @override void initState() { super.initState(); _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(); } @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)), ), ); } } // ── Monthly Report ────────────────────────────────────────────────────────── class _MonthlyReportSection extends StatelessWidget { const _MonthlyReportSection({required this.data}); final _DashboardData data; @override Widget build(BuildContext context) { final pct = data.changePercent; final isUp = pct >= 0; final pctStr = '${isUp ? '+' : ''}${pct.toStringAsFixed(0)}%'; return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.border), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ 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)), ], ), const SizedBox(height: 12), Row( children: [ Expanded( child: _MonthStat( label: 'Bu Ay', value: data.thisMonthDelivered, highlighted: true, ), ), const SizedBox(width: 12), Expanded( child: _MonthStat( label: 'Geçen Ay', value: data.lastMonthDelivered, highlighted: false, ), ), const SizedBox(width: 12), Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( color: isUp ? AppColors.successBg : AppColors.cancelledBg, borderRadius: BorderRadius.circular(8), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( isUp ? Icons.trending_up_rounded : Icons.trending_down_rounded, size: 16, color: isUp ? AppColors.success : AppColors.cancelled, ), const SizedBox(width: 4), Text( pctStr, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w700, color: isUp ? AppColors.success : AppColors.cancelled, ), ), ], ), ), ], ), ], ), ); } } class _MonthStat extends StatelessWidget { const _MonthStat({required this.label, required this.value, required this.highlighted}); final String label; final int value; final bool highlighted; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( 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, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: TextStyle(fontSize: 11, color: AppColors.textSecondary, fontWeight: FontWeight.w500)), const SizedBox(height: 2), Text( '$value iş', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w700, color: highlighted ? AppColors.accent : AppColors.textPrimary, ), ), ], ), ); } } // ── Gamification Row ───────────────────────────────────────────────────────── const _monthlyGoal = 50; class _GamificationRow extends StatelessWidget { const _GamificationRow({required this.data}); final _DashboardData data; @override Widget build(BuildContext context) { final progress = (data.thisMonthDelivered / _monthlyGoal).clamp(0.0, 1.0); final remaining = (_monthlyGoal - data.thisMonthDelivered).clamp(0, _monthlyGoal); return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.border), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( 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)), const Spacer(), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: AppColors.primary.withValues(alpha: 0.08), borderRadius: BorderRadius.circular(6), ), child: Text( '${data.points} puan', style: const TextStyle(fontSize: 11, fontWeight: FontWeight.w700, color: AppColors.primary), ), ), ], ), const SizedBox(height: 10), ClipRRect( borderRadius: BorderRadius.circular(6), child: LinearProgressIndicator( value: progress, minHeight: 8, backgroundColor: AppColors.background, valueColor: AlwaysStoppedAnimation( progress >= 1.0 ? AppColors.success : AppColors.accent, ), ), ), const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '${data.thisMonthDelivered} / $_monthlyGoal iş teslim edildi', style: 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, ), ), ], ), ], ), ); } } // ── Data Model ─────────────────────────────────────────────────────────────── class _DashboardData { final List pendingJobs; final List inProgressJobs; final List atLabJobs; final List atClinicJobs; final int sentCount; final int deliveredCount; final int thisMonthDelivered; final int lastMonthDelivered; const _DashboardData({ required this.pendingJobs, required this.inProgressJobs, required this.atLabJobs, required this.atClinicJobs, required this.sentCount, required this.deliveredCount, required this.thisMonthDelivered, required this.lastMonthDelivered, }); int get points => thisMonthDelivered * 10; double get changePercent => lastMonthDelivered == 0 ? (thisMonthDelivered > 0 ? 100 : 0) : (thisMonthDelivered - lastMonthDelivered) / lastMonthDelivered * 100; }