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/services/realtime_service.dart'; import '../../../core/theme/app_theme.dart'; import '../../../core/widgets/tooth_logo.dart'; import '../../../models/job.dart'; import '../jobs/clinic_jobs_repository.dart'; import '../patients/clinic_patients_repository.dart'; class ClinicDashboardScreen extends ConsumerStatefulWidget { const ClinicDashboardScreen({super.key}); @override ConsumerState createState() => _ClinicDashboardScreenState(); } class _ClinicDashboardScreenState extends ConsumerState { late Future<_DashboardData> _future; late UnsubFn _unsub; final Map _actingJobs = {}; @override void initState() { super.initState(); _load(); final tenantId = ref.read(authProvider).activeTenant!.tenant.id; _unsub = RealtimeService.instance.watch( 'jobs', filter: "clinic_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 = _loadAll(tenantId); }); } Future _approveAtClinic(Job job) async { final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: Text(job.patientCode), content: Text('${job.prostheticType.label} işini onaylıyor musunuz?'), actions: [ TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('İptal')), FilledButton( style: FilledButton.styleFrom(backgroundColor: AppColors.success), onPressed: () => Navigator.pop(ctx, true), child: const Text('Onayla'), ), ], ), ); if (confirmed != true || !mounted) return; setState(() => _actingJobs[job.id] = true); try { await ClinicJobsRepository.instance.approveAtClinic(job.id, job); _load(); } catch (e) { if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Hata: $e'))); } finally { if (mounted) setState(() => _actingJobs.remove(job.id)); } } Future _markDelivered(Job job) async { final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: Text(job.patientCode), content: Text('${job.prostheticType.label} işi teslim alındı olarak işaretlensin mi?'), actions: [ TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('İptal')), FilledButton( onPressed: () => Navigator.pop(ctx, true), child: const Text('Teslim Aldım'), ), ], ), ); if (confirmed != true || !mounted) return; setState(() => _actingJobs[job.id] = true); try { await ClinicJobsRepository.instance.markDelivered(job.id, job); _load(); } catch (e) { if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Hata: $e'))); } finally { if (mounted) setState(() => _actingJobs.remove(job.id)); } } Future<_DashboardData> _loadAll(String tenantId) async { final now = DateTime.now(); final thisMonthStart = DateTime(now.year, now.month, 1); final lastMonthStart = DateTime(now.year, now.month - 1, 1); final results = await Future.wait([ ClinicJobsRepository.instance.listOutbound(tenantId, statuses: ['pending'], limit: 200), ClinicJobsRepository.instance.listOutbound(tenantId, statuses: ['in_progress'], limit: 200), ClinicJobsRepository.instance.listOutbound(tenantId, statuses: ['sent'], limit: 200), ClinicJobsRepository.instance.listOutbound(tenantId, limit: 5), ClinicPatientsRepository.instance.listPatients(tenantId, limit: 200), ]); final thisMonth = await ClinicJobsRepository.instance.countDelivered(tenantId, from: thisMonthStart); final lastMonth = await ClinicJobsRepository.instance.countDelivered(tenantId, from: lastMonthStart, to: thisMonthStart); final inProgressJobs = results[1] as List; final sentJobs = results[2] as List; final provaAtClinic = inProgressJobs.where((j) => j.location == JobLocation.atClinic).toList(); final actionJobs = [...provaAtClinic, ...sentJobs]; return _DashboardData( pendingCount: (results[0] as List).length, inProgressCount: inProgressJobs.length, sentCount: sentJobs.length, patientCount: (results[4] as List).length, recentJobs: results[3] as List, thisMonthDelivered: thisMonth, lastMonthDelivered: lastMonth, actionJobs: actionJobs, ); } @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.pendingCount, inProgress: data.inProgressCount, sent: data.sentCount, patients: data.patientCount, ), ), ), 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), ), ), ], SliverPadding( padding: EdgeInsets.fromLTRB(hPad, 16, hPad, 0), sliver: SliverToBoxAdapter( child: FilledButton.icon( onPressed: () => context.push(routeClinicJobNew), icon: const Icon(Icons.add_rounded, size: 20), label: const Text('Yeni İş Oluştur'), style: FilledButton.styleFrom( minimumSize: const Size(double.infinity, 52), backgroundColor: AppColors.accent, ), ).animate().fadeIn(duration: 300.ms).slideY(begin: 0.1, end: 0), ), ), if (data.actionJobs.isNotEmpty) SliverPadding( padding: EdgeInsets.fromLTRB(hPad, 20, hPad, 0), sliver: SliverToBoxAdapter( child: _ActionSection( jobs: data.actionJobs, actingJobs: _actingJobs, onApprove: _approveAtClinic, onDeliver: _markDelivered, ).animate().fadeIn(duration: 300.ms).slideY(begin: 0.06, end: 0), ), ), SliverPadding( padding: EdgeInsets.fromLTRB(hPad, 20, hPad, 4), sliver: SliverToBoxAdapter( child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('Son İşler', style: Theme.of(context).textTheme.titleMedium), TextButton( onPressed: () => context.go(routeClinicJobs), style: TextButton.styleFrom( foregroundColor: AppColors.accent, padding: const EdgeInsets.symmetric(horizontal: 8), ), child: const Text('Tümünü Gör'), ), ], ), ), ), if (data.recentJobs.isEmpty) const SliverFillRemaining( hasScrollBody: false, child: _EmptyJobs()) else SliverPadding( padding: EdgeInsets.fromLTRB(hPad, 0, hPad, 24), sliver: SliverList.separated( itemCount: data.recentJobs.length, separatorBuilder: (_, __) => const SizedBox(height: 10), itemBuilder: (ctx, i) => _JobCard(job: data.recentJobs[i]) .animate(delay: (i * 60).ms) .fadeIn(duration: 300.ms) .slideY(begin: 0.12, end: 0), ), ), ], ); }, ), ); }, ), ); } } class _DashboardData { const _DashboardData({ required this.pendingCount, required this.inProgressCount, required this.sentCount, required this.patientCount, required this.recentJobs, required this.thisMonthDelivered, required this.lastMonthDelivered, required this.actionJobs, }); final int pendingCount; final int inProgressCount; final int sentCount; final int patientCount; final List recentJobs; final int thisMonthDelivered; final int lastMonthDelivered; final List actionJobs; int get points => thisMonthDelivered * 10; double get changePercent => lastMonthDelivered == 0 ? (thisMonthDelivered > 0 ? 100 : 0) : (thisMonthDelivered - lastMonthDelivered) / lastMonthDelivered * 100; } // ── Action Section ─────────────────────────────────────────────────────────── class _ActionSection extends StatelessWidget { const _ActionSection({ required this.jobs, required this.actingJobs, required this.onApprove, required this.onDeliver, }); final List jobs; final Map actingJobs; final Future Function(Job) onApprove; final Future Function(Job) onDeliver; @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( width: 26, height: 26, decoration: BoxDecoration(color: AppColors.pending, borderRadius: BorderRadius.circular(7)), child: const Icon(Icons.priority_high_rounded, size: 15, color: Colors.white), ), const SizedBox(width: 8), Text('Yapılacaklar', style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700)), const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2), decoration: BoxDecoration(color: AppColors.pending, borderRadius: BorderRadius.circular(10)), child: Text('${jobs.length}', style: const TextStyle(fontSize: 11, fontWeight: FontWeight.w800, color: Colors.white)), ), ], ), const SizedBox(height: 12), ...jobs.asMap().entries.map((entry) => Padding( padding: const EdgeInsets.only(bottom: 10), child: _ActionJobCard( job: entry.value, acting: actingJobs[entry.value.id] == true, onApprove: () => onApprove(entry.value), onDeliver: () => onDeliver(entry.value), ).animate(delay: (entry.key * 50).ms).fadeIn(duration: 250.ms).slideY(begin: 0.08, end: 0), )), ], ); } } class _ActionJobCard extends StatelessWidget { const _ActionJobCard({ required this.job, required this.acting, required this.onApprove, required this.onDeliver, }); final Job job; final bool acting; final VoidCallback onApprove; final VoidCallback onDeliver; bool get _isProva => job.status == JobStatus.inProgress && job.location == JobLocation.atClinic; @override Widget build(BuildContext context) { final isProva = _isProva; final borderColor = isProva ? AppColors.pending : AppColors.accent; final bgColor = isProva ? AppColors.pendingBg : AppColors.inProgressBg; final iconColor = isProva ? AppColors.pending : AppColors.accent; final icon = isProva ? Icons.rate_review_outlined : Icons.inventory_2_outlined; final statusLabel = isProva ? 'Onay Bekliyor' : 'Teslimat Bekliyor'; return Semantics( label: job.patientCode, button: true, excludeSemantics: true, child: Material( color: AppColors.surface, borderRadius: BorderRadius.circular(14), child: InkWell( onTap: () => context.push('/clinic/jobs/${job.id}'), borderRadius: BorderRadius.circular(14), child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(14), border: Border.all(color: borderColor.withValues(alpha: 0.45), width: 1.5), boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 3))], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.fromLTRB(14, 12, 14, 10), child: Row( children: [ Container( width: 40, height: 40, decoration: BoxDecoration(color: bgColor, borderRadius: BorderRadius.circular(11)), child: Icon(icon, color: iconColor, size: 19), ), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(job.patientCode, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.textPrimary)), const SizedBox(height: 2), Text( '${job.prostheticType.label} · ${job.labName ?? 'Lab'}', style: const TextStyle(fontSize: 12, color: AppColors.textSecondary), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ), const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration(color: bgColor, borderRadius: BorderRadius.circular(8)), child: Text(statusLabel, style: TextStyle(fontSize: 10, fontWeight: FontWeight.w700, color: iconColor)), ), ], ), ), Container( decoration: BoxDecoration( color: bgColor.withValues(alpha: 0.45), borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(13), bottomRight: Radius.circular(13)), ), padding: const EdgeInsets.fromLTRB(12, 8, 12, 10), child: acting ? const Center( child: Padding( padding: EdgeInsets.symmetric(vertical: 4), child: SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2.5, color: AppColors.accent)), ), ) : isProva ? Row(children: [ Expanded( child: FilledButton.icon( onPressed: onApprove, icon: const Icon(Icons.check_circle_outline, size: 15), label: const Text('Onayla', style: TextStyle(fontSize: 13)), style: FilledButton.styleFrom( backgroundColor: AppColors.success, minimumSize: const Size(0, 36), tapTargetSize: MaterialTapTargetSize.shrinkWrap, padding: const EdgeInsets.symmetric(horizontal: 12), ), ), ), const SizedBox(width: 8), OutlinedButton.icon( onPressed: () => context.push('/clinic/jobs/${job.id}'), icon: const Icon(Icons.open_in_new_rounded, size: 14), label: const Text('Detay', style: TextStyle(fontSize: 13)), style: OutlinedButton.styleFrom( minimumSize: const Size(0, 36), tapTargetSize: MaterialTapTargetSize.shrinkWrap, padding: const EdgeInsets.symmetric(horizontal: 12), foregroundColor: AppColors.pending, side: BorderSide(color: AppColors.pending.withValues(alpha: 0.6)), ), ), ]) : Row(children: [ Expanded( child: FilledButton.icon( onPressed: onDeliver, icon: const Icon(Icons.inventory_2_outlined, size: 15), label: const Text('Teslim Aldım', style: TextStyle(fontSize: 13)), style: FilledButton.styleFrom( minimumSize: const Size(0, 36), tapTargetSize: MaterialTapTargetSize.shrinkWrap, padding: const EdgeInsets.symmetric(horizontal: 12), ), ), ), const SizedBox(width: 8), OutlinedButton.icon( onPressed: () => context.push('/clinic/jobs/${job.id}'), icon: const Icon(Icons.open_in_new_rounded, size: 14), label: const Text('Detay', style: TextStyle(fontSize: 13)), style: OutlinedButton.styleFrom( minimumSize: const Size(0, 36), tapTargetSize: MaterialTapTargetSize.shrinkWrap, padding: const EdgeInsets.symmetric(horizontal: 12), foregroundColor: AppColors.accent, side: BorderSide(color: AppColors.accent.withValues(alpha: 0.6)), ), ), ]), ), ], ), ), ), ), ); } } // ── 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 = 20; 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, ), ), ], ), ], ), ); } } // ── Header ────────────────────────────────────────────────────────────────── class _DashboardHeader extends StatelessWidget { const _DashboardHeader({required this.companyName}); final String companyName; 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(routeClinicSettings), 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(routeClinicSettings), 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)), ], ), ), ), ), ); } } // ── Stats ──────────────────────────────────────────────────────────────────── class _StatsRow extends StatelessWidget { const _StatsRow({ required this.pending, required this.inProgress, required this.sent, required this.patients, }); final int pending; final int inProgress; final int sent; final int patients; @override Widget build(BuildContext context) { final isWideDesktop = MediaQuery.sizeOf(context).width >= AppLayout.wideBreakpoint; final c1 = _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 c2 = _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 c3 = _StatCard(label: 'Toplam Hasta', value: '$patients', icon: Icons.people_outline_rounded, color: AppColors.success, bgColor: AppColors.successBg) .animate(delay: 160.ms).fadeIn(duration: 350.ms).slideY(begin: 0.2, end: 0); // Wide desktop (≥ 1100px): 4 cards side by side — full lifecycle view. if (isWideDesktop) { final c4 = _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); return Row( children: [ Expanded(child: c1), const SizedBox(width: 12), Expanded(child: c2), const SizedBox(width: 12), Expanded(child: c4), const SizedBox(width: 12), Expanded(child: c3), ], ); } // Mobile + narrow sidebar (< 1100px): 2+1 column layout. return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( children: [ Expanded(child: c1), const SizedBox(width: 12), Expanded(child: c2), ], ), const SizedBox(height: 12), c3, ], ); } } 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)), ], ), ], ), ); } } // ── Job Card ───────────────────────────────────────────────────────────────── 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; final statusColor = _statusColor(job.status); final statusBg = _statusBg(job.status); return Semantics( label: job.patientCode, button: true, excludeSemantics: true, child: Material( color: AppColors.surface, borderRadius: BorderRadius.circular(14), child: InkWell( onTap: () => context.push('/clinic/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: statusBg, borderRadius: BorderRadius.circular(12)), child: Icon(Icons.work_outline_rounded, color: statusColor, size: 22), ), 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.labName ?? 'Laboratuvar', 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), _Tag( label: job.status.label, color: statusColor, bg: statusBg), ], ), ], ), ), 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), ), ], ), ], ], ), ), ), ), ); } Color _statusColor(JobStatus s) { switch (s) { case JobStatus.pending: return AppColors.pending; case JobStatus.inProgress: return AppColors.inProgress; case JobStatus.sent: return AppColors.accent; case JobStatus.delivered: return AppColors.success; case JobStatus.cancelled: return AppColors.cancelled; } } Color _statusBg(JobStatus s) { switch (s) { case JobStatus.pending: return AppColors.pendingBg; case JobStatus.inProgress: return AppColors.inProgressBg; case JobStatus.sent: return AppColors.inProgressBg; case JobStatus.delivered: return AppColors.successBg; case JobStatus.cancelled: return AppColors.cancelledBg; } } } 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)), ); } } // ── Empty / Error / Skeleton ───────────────────────────────────────────────── class _EmptyJobs extends StatelessWidget { const _EmptyJobs(); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.all(40), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: 72, height: 72, decoration: BoxDecoration( color: AppColors.inProgressBg, borderRadius: BorderRadius.circular(20)), child: const Icon(Icons.work_off_outlined, color: AppColors.inProgress, size: 32), ), const SizedBox(height: 16), const Text('Henüz iş yok', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.textPrimary)), const SizedBox(height: 6), const Text( 'Yeni iş oluşturduğunuzda\nburada görünecek', textAlign: TextAlign.center, style: TextStyle(fontSize: 13, 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: Column( children: [ Row(children: [ Expanded(child: _ShimmerBox(height: 84, radius: 16)), SizedBox(width: 12), Expanded(child: _ShimmerBox(height: 84, radius: 16)), ]), SizedBox(height: 12), _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)), ), ); } }