From 65a42039ee30758da737644c6495309f197265a0 Mon Sep 17 00:00:00 2001 From: Marc Rejohn Castillano Date: Sat, 6 Jun 2026 18:29:56 +0800 Subject: [PATCH] Enhanced Leave and Pass Slip UI/UX --- lib/screens/attendance/attendance_screen.dart | 895 ++++++++++++------ 1 file changed, 600 insertions(+), 295 deletions(-) diff --git a/lib/screens/attendance/attendance_screen.dart b/lib/screens/attendance/attendance_screen.dart index e407c950..34d0dcbe 100644 --- a/lib/screens/attendance/attendance_screen.dart +++ b/lib/screens/attendance/attendance_screen.dart @@ -49,6 +49,7 @@ import '../../widgets/app_breakpoints.dart'; import '../../widgets/app_page_header.dart'; import '../../widgets/responsive_body.dart'; import '../../widgets/sync_pending_badge.dart'; +import '../../widgets/m3_card.dart'; class AttendanceScreen extends ConsumerStatefulWidget { const AttendanceScreen({super.key}); @@ -5631,8 +5632,9 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> { cardTitle = 'Active Pass Slip'; } - return Card( + return M3Card.elevated( color: cardColor, + margin: EdgeInsets.zero, child: Padding( padding: const EdgeInsets.all(16), child: Column( @@ -5640,8 +5642,12 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> { children: [ Row( children: [ - Icon(cardIcon, color: onCardColor), - const SizedBox(width: 8), + CircleAvatar( + radius: 18, + backgroundColor: onCardColor.withValues(alpha: 0.15), + child: Icon(cardIcon, color: onCardColor, size: 18), + ), + const SizedBox(width: 12), Expanded( child: Text( cardTitle, @@ -5653,16 +5659,26 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> { ), ], ), - const SizedBox(height: 8), + const SizedBox(height: 10), Text( - 'Reason: ${activeSlip.reason}', - style: theme.textTheme.bodyMedium, + activeSlip.reason, + style: theme.textTheme.bodyMedium?.copyWith(color: onCardColor), ), - if (activeSlip.slipStart != null && hasStarted) - Text( - 'Started: ${AppTime.formatTime(activeSlip.slipStart!)}', - style: theme.textTheme.bodySmall, + if (activeSlip.slipStart != null && hasStarted) ...[ + const SizedBox(height: 4), + Row( + children: [ + Icon(Icons.schedule, size: 14, color: onCardColor.withValues(alpha: 0.7)), + const SizedBox(width: 4), + Text( + 'Started: ${AppTime.formatTime(activeSlip.slipStart!)}', + style: theme.textTheme.bodySmall?.copyWith( + color: onCardColor.withValues(alpha: 0.85), + ), + ), + ], ), + ], const SizedBox(height: 12), SizedBox( width: double.infinity, @@ -5670,7 +5686,7 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> { onPressed: _submitting ? null : () => _completeSlip(activeSlip.id), - icon: const Icon(Icons.check), + icon: const Icon(Icons.check_circle_outline), label: const Text('Complete / Return'), ), ), @@ -5686,11 +5702,24 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> { // Pending slips for admin approval if (isAdmin) ...[ - Text( - 'Pending Approvals', - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, - ), + Row( + children: [ + Container( + width: 4, + height: 20, + decoration: BoxDecoration( + color: colors.tertiary, + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(width: 10), + Text( + 'Pending Approvals', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ], ), const SizedBox(height: 8), for (int i = 0; i < pendingSlipList.length; i++) @@ -5706,23 +5735,42 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> { ), if (pendingSlipList.isEmpty) Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Text( - 'No pending pass slip requests.', - style: theme.textTheme.bodyMedium?.copyWith( - color: colors.onSurfaceVariant, - ), + padding: const EdgeInsets.symmetric(vertical: 12), + child: Row( + children: [ + Icon(Icons.inbox_outlined, size: 20, color: colors.onSurfaceVariant), + const SizedBox(width: 8), + Text( + 'No pending pass slip requests.', + style: theme.textTheme.bodyMedium?.copyWith( + color: colors.onSurfaceVariant, + ), + ), + ], ), ), const SizedBox(height: 16), ], // History - Text( - 'Pass Slip History', - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, - ), + Row( + children: [ + Container( + width: 4, + height: 20, + decoration: BoxDecoration( + color: colors.outlineVariant, + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(width: 10), + Text( + 'Pass Slip History', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ], ), const SizedBox(height: 8), for (int i = 0; i < historySlipList.length; i++) @@ -5739,12 +5787,22 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> { if (slips.isEmpty) Center( child: Padding( - padding: const EdgeInsets.symmetric(vertical: 24), - child: Text( - 'No pass slip records.', - style: theme.textTheme.bodyMedium?.copyWith( - color: colors.onSurfaceVariant, - ), + padding: const EdgeInsets.symmetric(vertical: 32), + child: Column( + children: [ + Icon( + Icons.receipt_long_outlined, + size: 48, + color: colors.onSurfaceVariant.withValues(alpha: 0.5), + ), + const SizedBox(height: 12), + Text( + 'No pass slip records.', + style: theme.textTheme.bodyMedium?.copyWith( + color: colors.onSurfaceVariant, + ), + ), + ], ), ), ), @@ -5769,98 +5827,131 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> { final name = p?.fullName ?? slip.userId; Color statusColor; + IconData statusIcon; switch (slip.status) { case 'approved': statusColor = Colors.green; + statusIcon = Icons.check_circle; case 'rejected': statusColor = colors.error; + statusIcon = Icons.cancel; case 'completed': statusColor = colors.primary; + statusIcon = Icons.task_alt; default: statusColor = Colors.orange; + statusIcon = Icons.hourglass_top; } return Padding( padding: const EdgeInsets.only(bottom: 8), - child: Card( + child: M3Card.outlined( child: Padding( - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded(child: Text(name, style: theme.textTheme.titleSmall)), - if (isPending) ...[ - SyncPendingBadge(isPending: true, compact: true), - const SizedBox(width: 8), - ], - Container( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 4, - ), - decoration: BoxDecoration( - color: statusColor.withValues(alpha: 0.15), - borderRadius: BorderRadius.circular(12), - ), - child: Text( - slip.status.toUpperCase(), - style: theme.textTheme.labelSmall?.copyWith( - color: statusColor, - fontWeight: FontWeight.w600, + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + name, + style: theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600), ), ), + if (isPending) ...[ + SyncPendingBadge(isPending: true, compact: true), + const SizedBox(width: 8), + ], + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: statusColor.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(statusIcon, size: 12, color: statusColor), + const SizedBox(width: 4), + Text( + slip.status.toUpperCase(), + style: theme.textTheme.labelSmall?.copyWith( + color: statusColor, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 6), + Text(slip.reason, style: theme.textTheme.bodyMedium), + const SizedBox(height: 4), + Row( + children: [ + Icon(Icons.schedule, size: 13, color: colors.onSurfaceVariant), + const SizedBox(width: 4), + Text( + 'Requested: ${AppTime.formatDate(slip.requestedAt)} ${AppTime.formatTime(slip.requestedAt)}', + style: theme.textTheme.bodySmall?.copyWith(color: colors.onSurfaceVariant), + ), + ], + ), + if (slip.requestedStart != null) ...[ + const SizedBox(height: 2), + Row( + children: [ + Icon(Icons.alarm, size: 13, color: colors.onSurfaceVariant), + const SizedBox(width: 4), + Text( + 'Preferred start: ${AppTime.formatTime(slip.requestedStart!)}', + style: theme.textTheme.bodySmall?.copyWith(color: colors.onSurfaceVariant), + ), + ], ), ], - ), - const SizedBox(height: 4), - Text(slip.reason, style: theme.textTheme.bodyMedium), - Text( - 'Requested: ${AppTime.formatDate(slip.requestedAt)} ${AppTime.formatTime(slip.requestedAt)}', - style: theme.textTheme.bodySmall?.copyWith( - color: colors.onSurfaceVariant, - ), - ), - if (slip.requestedStart != null) - Text( - 'Preferred start: ${AppTime.formatTime(slip.requestedStart!)}', - style: theme.textTheme.bodySmall?.copyWith( - color: colors.onSurfaceVariant, - ), - ), - if (slip.slipStart != null) - Text( - 'Started: ${AppTime.formatTime(slip.slipStart!)}' - '${slip.slipEnd != null ? " · Ended: ${AppTime.formatTime(slip.slipEnd!)}" : ""}', - style: theme.textTheme.bodySmall?.copyWith( - color: colors.onSurfaceVariant, - ), - ), - if (showActions && slip.status == 'pending') ...[ - const SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: _submitting ? null : () => _rejectSlip(slip.id), - child: Text( - 'Reject', - style: TextStyle(color: colors.error), + if (slip.slipStart != null) ...[ + const SizedBox(height: 2), + Row( + children: [ + Icon(Icons.directions_walk, size: 13, color: colors.onSurfaceVariant), + const SizedBox(width: 4), + Text( + 'Started: ${AppTime.formatTime(slip.slipStart!)}' + '${slip.slipEnd != null ? " · Ended: ${AppTime.formatTime(slip.slipEnd!)}" : ""}', + style: theme.textTheme.bodySmall?.copyWith(color: colors.onSurfaceVariant), ), - ), - const SizedBox(width: 8), - FilledButton( - onPressed: _submitting ? null : () => _approveSlip(slip.id), - child: const Text('Approve'), - ), - ], - ), + ], + ), + ], + if (showActions && slip.status == 'pending') ...[ + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + OutlinedButton.icon( + onPressed: _submitting ? null : () => _rejectSlip(slip.id), + icon: Icon(Icons.close, size: 16, color: colors.error), + label: Text('Reject', style: TextStyle(color: colors.error)), + style: OutlinedButton.styleFrom( + side: BorderSide(color: colors.error.withValues(alpha: 0.5)), + ), + ), + const SizedBox(width: 8), + FilledButton.icon( + onPressed: _submitting ? null : () => _approveSlip(slip.id), + icon: const Icon(Icons.check, size: 16), + label: const Text('Approve'), + ), + ], + ), + ], ], - ], + ), ), ), - ), ); } @@ -5976,21 +6067,40 @@ class _LeaveTabState extends ConsumerState<_LeaveTab> { children: [ // ── Pending Approvals (admin only) ── if (isAdmin) ...[ - Text( - 'Pending Approvals', - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, - ), + Row( + children: [ + Container( + width: 4, + height: 20, + decoration: BoxDecoration( + color: colors.tertiary, + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(width: 10), + Text( + 'Pending Approvals', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ], ), const SizedBox(height: 8), if (pendingApprovals.isEmpty) Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Text( - 'No pending leave requests.', - style: theme.textTheme.bodyMedium?.copyWith( - color: colors.onSurfaceVariant, - ), + padding: const EdgeInsets.symmetric(vertical: 12), + child: Row( + children: [ + Icon(Icons.inbox_outlined, size: 20, color: colors.onSurfaceVariant), + const SizedBox(width: 8), + Text( + 'No pending leave requests.', + style: theme.textTheme.bodyMedium?.copyWith( + color: colors.onSurfaceVariant, + ), + ), + ], ), ), for (int i = 0; i < pendingApprovals.length; i++) @@ -6008,22 +6118,45 @@ class _LeaveTabState extends ConsumerState<_LeaveTab> { ], // ── My Leave Applications ── - Text( - 'My Leave Applications', - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, - ), + Row( + children: [ + Container( + width: 4, + height: 20, + decoration: BoxDecoration( + color: colors.secondary, + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(width: 10), + Text( + 'My Leave Applications', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ], ), const SizedBox(height: 8), if (myLeaves.isEmpty) Center( child: Padding( - padding: const EdgeInsets.symmetric(vertical: 24), - child: Text( - 'You have no leave applications.', - style: theme.textTheme.bodyMedium?.copyWith( - color: colors.onSurfaceVariant, - ), + padding: const EdgeInsets.symmetric(vertical: 32), + child: Column( + children: [ + Icon( + Icons.event_note_outlined, + size: 48, + color: colors.onSurfaceVariant.withValues(alpha: 0.5), + ), + const SizedBox(height: 12), + Text( + 'You have no leave applications.', + style: theme.textTheme.bodyMedium?.copyWith( + color: colors.onSurfaceVariant, + ), + ), + ], ), ), ), @@ -6042,11 +6175,24 @@ class _LeaveTabState extends ConsumerState<_LeaveTab> { // ── All Leave History (admin only) ── if (isAdmin) ...[ const SizedBox(height: 24), - Text( - 'All Leave History', - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, - ), + Row( + children: [ + Container( + width: 4, + height: 20, + decoration: BoxDecoration( + color: colors.outlineVariant, + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(width: 10), + Text( + 'All Leave History', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ], ), const SizedBox(height: 8), for (int i = 0; i < allLeaveHistory.length; i++) @@ -6062,12 +6208,18 @@ class _LeaveTabState extends ConsumerState<_LeaveTab> { ), if (allLeaveHistory.isEmpty) Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Text( - 'No leave history from other staff.', - style: theme.textTheme.bodyMedium?.copyWith( - color: colors.onSurfaceVariant, - ), + padding: const EdgeInsets.symmetric(vertical: 12), + child: Row( + children: [ + Icon(Icons.history_outlined, size: 20, color: colors.onSurfaceVariant), + const SizedBox(width: 8), + Text( + 'No leave history from other staff.', + style: theme.textTheme.bodyMedium?.copyWith( + color: colors.onSurfaceVariant, + ), + ), + ], ), ), ], @@ -6093,109 +6245,156 @@ class _LeaveTabState extends ConsumerState<_LeaveTab> { final name = p?.fullName ?? leave.userId; Color statusColor; + IconData statusIcon; switch (leave.status) { case 'approved': statusColor = Colors.teal; + statusIcon = Icons.check_circle; case 'rejected': statusColor = colors.error; + statusIcon = Icons.cancel; case 'cancelled': statusColor = colors.onSurfaceVariant; + statusIcon = Icons.remove_circle_outline; default: statusColor = Colors.orange; + statusIcon = Icons.hourglass_top; } + const leaveTypeMeta = { + 'emergency_leave': (Icons.warning_amber_rounded, Color(0xFFE65100)), + 'parental_leave': (Icons.child_care_rounded, Color(0xFFAD1457)), + 'sick_leave': (Icons.local_hospital_rounded, Color(0xFFC62828)), + 'vacation_leave': (Icons.beach_access_rounded, Color(0xFF00695C)), + }; + final meta = leaveTypeMeta[leave.leaveType]; + final typeIcon = meta?.$1 ?? Icons.event_note; + final typeColor = meta?.$2 ?? colors.primary; + return Padding( padding: const EdgeInsets.only(bottom: 8), - child: Card( - child: Padding( - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded(child: Text(name, style: theme.textTheme.titleSmall)), - if (isPending) ...[ - SyncPendingBadge(isPending: true, compact: true), - const SizedBox(width: 8), + child: M3Card.outlined( + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + name, + style: theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600), + ), + ), + if (isPending) ...[ + SyncPendingBadge(isPending: true, compact: true), + const SizedBox(width: 8), + ], + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: statusColor.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(statusIcon, size: 12, color: statusColor), + const SizedBox(width: 4), + Text( + leave.status.toUpperCase(), + style: theme.textTheme.labelSmall?.copyWith( + color: statusColor, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), ], - Container( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 4, + ), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: typeColor.withValues(alpha: 0.10), + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(typeIcon, size: 13, color: typeColor), + const SizedBox(width: 5), + Text( + leave.leaveTypeLabel, + style: theme.textTheme.labelSmall?.copyWith( + color: typeColor, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + const SizedBox(height: 6), + Text(leave.justification, style: theme.textTheme.bodyMedium), + const SizedBox(height: 4), + Row( + children: [ + Icon(Icons.calendar_month, size: 13, color: colors.onSurfaceVariant), + const SizedBox(width: 4), + Expanded( + child: Text( + '${AppTime.formatDate(leave.startTime)} ' + '${AppTime.formatTime(leave.startTime)} – ' + '${AppTime.formatTime(leave.endTime)}', + style: theme.textTheme.bodySmall?.copyWith(color: colors.onSurfaceVariant), + ), ), - decoration: BoxDecoration( - color: statusColor.withValues(alpha: 0.15), - borderRadius: BorderRadius.circular(12), - ), - child: Text( - leave.status.toUpperCase(), - style: theme.textTheme.labelSmall?.copyWith( - color: statusColor, - fontWeight: FontWeight.w600, + ], + ), + // Approve / Reject for admins on pending leaves + if (showApproval && leave.status == 'pending') ...[ + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + OutlinedButton.icon( + onPressed: _submitting ? null : () => _rejectLeave(leave.id), + icon: Icon(Icons.close, size: 16, color: colors.error), + label: Text('Reject', style: TextStyle(color: colors.error)), + style: OutlinedButton.styleFrom( + side: BorderSide(color: colors.error.withValues(alpha: 0.5)), + ), + ), + const SizedBox(width: 8), + FilledButton.icon( + onPressed: _submitting ? null : () => _approveLeave(leave.id), + icon: const Icon(Icons.check, size: 16), + label: const Text('Approve'), + ), + ], + ), + ], + // Cancel future approved leaves + if (!showApproval && _canCancelFutureApproved(leave)) ...[ + const SizedBox(height: 8), + Align( + alignment: Alignment.centerRight, + child: OutlinedButton.icon( + onPressed: _submitting ? null : () => _cancelLeave(leave.id), + icon: Icon(Icons.event_busy, size: 16, color: colors.error), + label: Text('Cancel Leave', style: TextStyle(color: colors.error)), + style: OutlinedButton.styleFrom( + side: BorderSide(color: colors.error.withValues(alpha: 0.5)), ), ), ), ], - ), - const SizedBox(height: 4), - Text( - leave.leaveTypeLabel, - style: theme.textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w500, - ), - ), - Text(leave.justification, style: theme.textTheme.bodyMedium), - Text( - '${AppTime.formatDate(leave.startTime)} ' - '${AppTime.formatTime(leave.startTime)} – ' - '${AppTime.formatTime(leave.endTime)}', - style: theme.textTheme.bodySmall?.copyWith( - color: colors.onSurfaceVariant, - ), - ), - // Approve / Reject for admins on pending leaves - if (showApproval && leave.status == 'pending') ...[ - const SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: _submitting - ? null - : () => _rejectLeave(leave.id), - child: Text( - 'Reject', - style: TextStyle(color: colors.error), - ), - ), - const SizedBox(width: 8), - FilledButton( - onPressed: _submitting - ? null - : () => _approveLeave(leave.id), - child: const Text('Approve'), - ), - ], - ), ], - // Cancel future approved leaves: - // - user can cancel own - // - admin can cancel anyone - if (!showApproval && _canCancelFutureApproved(leave)) ...[ - const SizedBox(height: 8), - Align( - alignment: Alignment.centerRight, - child: TextButton( - onPressed: _submitting ? null : () => _cancelLeave(leave.id), - child: Text('Cancel', style: TextStyle(color: colors.error)), - ), - ), - ], - ], + ), ), ), - ), ); } @@ -6395,12 +6594,26 @@ class _PassSlipDialogState extends ConsumerState<_PassSlipDialog> { Widget build(BuildContext context) { final theme = Theme.of(context); + final colors = theme.colorScheme; + final content = Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Request Pass Slip', style: theme.textTheme.headlineSmall), - const SizedBox(height: 16), + Row( + children: [ + CircleAvatar( + radius: 22, + backgroundColor: colors.tertiaryContainer, + child: Icon(Icons.receipt_long, color: colors.onTertiaryContainer, size: 22), + ), + const SizedBox(width: 14), + Expanded( + child: Text('Request Pass Slip', style: theme.textTheme.headlineSmall), + ), + ], + ), + const SizedBox(height: 20), Row( children: [ Expanded( @@ -6433,8 +6646,7 @@ class _PassSlipDialogState extends ConsumerState<_PassSlipDialog> { ], ), const SizedBox(height: 16), - InkWell( - borderRadius: BorderRadius.circular(12), + M3Card.outlined( onTap: _submitting ? null : () async { @@ -6446,32 +6658,43 @@ class _PassSlipDialogState extends ConsumerState<_PassSlipDialog> { setState(() => _requestedStartTime = picked); } }, - child: InputDecorator( - decoration: InputDecoration( - labelText: 'Preferred start time (optional)', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - ), - prefixIcon: const Icon(Icons.schedule), - suffixIcon: _requestedStartTime != null - ? IconButton( - icon: const Icon(Icons.clear), - onPressed: _submitting - ? null - : () => setState(() => _requestedStartTime = null), - tooltip: 'Clear', - ) - : null, - ), - child: Text( - _requestedStartTime != null - ? _requestedStartTime!.format(context) - : 'Immediately upon approval', - style: theme.textTheme.bodyLarge?.copyWith( - color: _requestedStartTime != null - ? null - : theme.colorScheme.onSurfaceVariant, - ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + Icon(Icons.schedule, color: colors.primary, size: 20), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Preferred start time (optional)', + style: theme.textTheme.labelSmall?.copyWith( + color: colors.onSurfaceVariant, + ), + ), + const SizedBox(height: 2), + Text( + _requestedStartTime != null + ? _requestedStartTime!.format(context) + : 'Immediately upon approval', + style: theme.textTheme.bodyMedium?.copyWith( + color: _requestedStartTime != null ? null : colors.onSurfaceVariant, + ), + ), + ], + ), + ), + if (_requestedStartTime != null) + IconButton( + icon: const Icon(Icons.clear, size: 18), + onPressed: _submitting ? null : () => setState(() => _requestedStartTime = null), + tooltip: 'Clear', + visualDensity: VisualDensity.compact, + padding: EdgeInsets.zero, + ), + ], ), ), ), @@ -6484,15 +6707,16 @@ class _PassSlipDialogState extends ConsumerState<_PassSlipDialog> { child: const Text('Cancel'), ), const SizedBox(width: 8), - FilledButton( + FilledButton.icon( onPressed: _submitting ? null : _submit, - child: _submitting + icon: _submitting ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ) - : const Text('Submit'), + : const Icon(Icons.send, size: 18), + label: const Text('Submit'), ), ], ), @@ -6685,65 +6909,133 @@ class _FileLeaveDialogState extends ConsumerState<_FileLeaveDialog> { final theme = Theme.of(context); final colors = theme.colorScheme; + const leaveTypeMeta = { + 'emergency_leave': (Icons.warning_amber_rounded, Color(0xFFE65100)), + 'parental_leave': (Icons.child_care_rounded, Color(0xFFAD1457)), + 'sick_leave': (Icons.local_hospital_rounded, Color(0xFFC62828)), + 'vacation_leave': (Icons.beach_access_rounded, Color(0xFF00695C)), + }; + final content = Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('File Leave of Absence', style: theme.textTheme.headlineSmall), + Row( + children: [ + CircleAvatar( + radius: 22, + backgroundColor: colors.secondaryContainer, + child: Icon(Icons.event_busy, color: colors.onSecondaryContainer, size: 22), + ), + const SizedBox(width: 14), + Expanded( + child: Text('File Leave of Absence', style: theme.textTheme.headlineSmall), + ), + ], + ), + const SizedBox(height: 20), + + // Leave type chips + Text( + 'Leave Type', + style: theme.textTheme.labelMedium?.copyWith(color: colors.onSurfaceVariant), + ), + const SizedBox(height: 8), + Wrap( + spacing: 8, + runSpacing: 8, + children: _leaveTypes.entries.map((e) { + final meta = leaveTypeMeta[e.key]; + final typeIcon = meta?.$1 ?? Icons.event_note; + final typeColor = meta?.$2 ?? colors.primary; + final isSelected = _leaveType == e.key; + return ChoiceChip( + avatar: Icon( + typeIcon, + size: 16, + color: isSelected ? colors.onPrimaryContainer : typeColor, + ), + label: Text(e.value), + selected: isSelected, + selectedColor: colors.primaryContainer, + onSelected: (_) => setState(() => _leaveType = e.key), + ); + }).toList(), + ), const SizedBox(height: 16), - // Leave type - DropdownButtonFormField( - // ignore: deprecated_member_use - value: _leaveType, - decoration: const InputDecoration(labelText: 'Leave Type'), - items: _leaveTypes.entries - .map((e) => DropdownMenuItem(value: e.key, child: Text(e.value))) - .toList(), - onChanged: (v) { - if (v != null) setState(() => _leaveType = v); - }, - ), - const SizedBox(height: 12), - - // Date picker - ListTile( - contentPadding: EdgeInsets.zero, - leading: const Icon(Icons.calendar_today), - title: Text( - _startDate == null ? 'Select Date' : AppTime.formatDate(_startDate!), - ), - subtitle: const Text('Current or future dates only'), + // Date picker card + M3Card.outlined( onTap: _pickDate, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + Icon(Icons.calendar_today, color: colors.primary, size: 20), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Date', + style: theme.textTheme.labelSmall?.copyWith(color: colors.onSurfaceVariant), + ), + const SizedBox(height: 2), + Text( + _startDate == null ? 'Select date' : AppTime.formatDate(_startDate!), + style: theme.textTheme.bodyMedium?.copyWith( + color: _startDate == null ? colors.onSurfaceVariant : null, + ), + ), + ], + ), + ), + Icon(Icons.chevron_right, size: 18, color: colors.onSurfaceVariant), + ], + ), + ), ), + const SizedBox(height: 10), - // Time range + // Time range buttons Row( children: [ Expanded( - child: ListTile( - contentPadding: EdgeInsets.zero, - leading: const Icon(Icons.access_time), - title: Text(_startTime == null ? 'Start Time' : _startTime!.format(context)), - onTap: _pickStartTime, + child: OutlinedButton.icon( + onPressed: _pickStartTime, + icon: Icon(Icons.access_time, size: 16, color: colors.primary), + label: Text( + _startTime == null ? 'Start Time' : _startTime!.format(context), + style: theme.textTheme.bodyMedium, + ), + style: OutlinedButton.styleFrom( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14), + ), ), ), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 8), - child: Icon(Icons.arrow_forward), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Icon(Icons.arrow_forward, size: 16, color: colors.onSurfaceVariant), ), Expanded( - child: ListTile( - contentPadding: EdgeInsets.zero, - leading: const Icon(Icons.access_time), - title: Text(_endTime == null ? 'End Time' : _endTime!.format(context)), - subtitle: const Text('From shift schedule'), - onTap: _pickEndTime, + child: OutlinedButton.icon( + onPressed: _pickEndTime, + icon: Icon(Icons.access_time, size: 16, color: colors.primary), + label: Text( + _endTime == null ? 'End Time' : _endTime!.format(context), + style: theme.textTheme.bodyMedium, + ), + style: OutlinedButton.styleFrom( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14), + ), ), ), ], ), - const SizedBox(height: 12), + const SizedBox(height: 16), // Justification with AI Row( @@ -6779,14 +7071,27 @@ class _FileLeaveDialogState extends ConsumerState<_FileLeaveDialog> { ), const SizedBox(height: 16), - if (widget.isAdmin) - Padding( - padding: const EdgeInsets.only(bottom: 8), - child: Text( - 'As admin, your leave will be auto-approved.', - style: theme.textTheme.bodySmall?.copyWith(color: colors.primary), + if (widget.isAdmin) ...[ + M3Card.filled( + color: colors.primaryContainer, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + child: Row( + children: [ + Icon(Icons.verified_user, size: 16, color: colors.onPrimaryContainer), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Your leave will be auto-approved.', + style: theme.textTheme.bodySmall?.copyWith(color: colors.onPrimaryContainer), + ), + ), + ], + ), ), ), + const SizedBox(height: 12), + ], // Actions Row(