Enhanced Leave and Pass Slip UI/UX

This commit is contained in:
2026-06-06 18:29:56 +08:00
parent d813ee45a2
commit 65a42039ee
+458 -153
View File
@@ -49,6 +49,7 @@ import '../../widgets/app_breakpoints.dart';
import '../../widgets/app_page_header.dart'; import '../../widgets/app_page_header.dart';
import '../../widgets/responsive_body.dart'; import '../../widgets/responsive_body.dart';
import '../../widgets/sync_pending_badge.dart'; import '../../widgets/sync_pending_badge.dart';
import '../../widgets/m3_card.dart';
class AttendanceScreen extends ConsumerStatefulWidget { class AttendanceScreen extends ConsumerStatefulWidget {
const AttendanceScreen({super.key}); const AttendanceScreen({super.key});
@@ -5631,8 +5632,9 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> {
cardTitle = 'Active Pass Slip'; cardTitle = 'Active Pass Slip';
} }
return Card( return M3Card.elevated(
color: cardColor, color: cardColor,
margin: EdgeInsets.zero,
child: Padding( child: Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( child: Column(
@@ -5640,8 +5642,12 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> {
children: [ children: [
Row( Row(
children: [ children: [
Icon(cardIcon, color: onCardColor), CircleAvatar(
const SizedBox(width: 8), radius: 18,
backgroundColor: onCardColor.withValues(alpha: 0.15),
child: Icon(cardIcon, color: onCardColor, size: 18),
),
const SizedBox(width: 12),
Expanded( Expanded(
child: Text( child: Text(
cardTitle, cardTitle,
@@ -5653,16 +5659,26 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> {
), ),
], ],
), ),
const SizedBox(height: 8), const SizedBox(height: 10),
Text( Text(
'Reason: ${activeSlip.reason}', activeSlip.reason,
style: theme.textTheme.bodyMedium, style: theme.textTheme.bodyMedium?.copyWith(color: onCardColor),
), ),
if (activeSlip.slipStart != null && hasStarted) 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( Text(
'Started: ${AppTime.formatTime(activeSlip.slipStart!)}', 'Started: ${AppTime.formatTime(activeSlip.slipStart!)}',
style: theme.textTheme.bodySmall, style: theme.textTheme.bodySmall?.copyWith(
color: onCardColor.withValues(alpha: 0.85),
), ),
),
],
),
],
const SizedBox(height: 12), const SizedBox(height: 12),
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
@@ -5670,7 +5686,7 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> {
onPressed: _submitting onPressed: _submitting
? null ? null
: () => _completeSlip(activeSlip.id), : () => _completeSlip(activeSlip.id),
icon: const Icon(Icons.check), icon: const Icon(Icons.check_circle_outline),
label: const Text('Complete / Return'), label: const Text('Complete / Return'),
), ),
), ),
@@ -5686,12 +5702,25 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> {
// Pending slips for admin approval // Pending slips for admin approval
if (isAdmin) ...[ if (isAdmin) ...[
Row(
children: [
Container(
width: 4,
height: 20,
decoration: BoxDecoration(
color: colors.tertiary,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 10),
Text( Text(
'Pending Approvals', 'Pending Approvals',
style: theme.textTheme.titleMedium?.copyWith( style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
), ),
],
),
const SizedBox(height: 8), const SizedBox(height: 8),
for (int i = 0; i < pendingSlipList.length; i++) for (int i = 0; i < pendingSlipList.length; i++)
M3FadeSlideIn( M3FadeSlideIn(
@@ -5706,24 +5735,43 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> {
), ),
if (pendingSlipList.isEmpty) if (pendingSlipList.isEmpty)
Padding( Padding(
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(vertical: 12),
child: Text( child: Row(
children: [
Icon(Icons.inbox_outlined, size: 20, color: colors.onSurfaceVariant),
const SizedBox(width: 8),
Text(
'No pending pass slip requests.', 'No pending pass slip requests.',
style: theme.textTheme.bodyMedium?.copyWith( style: theme.textTheme.bodyMedium?.copyWith(
color: colors.onSurfaceVariant, color: colors.onSurfaceVariant,
), ),
), ),
],
),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
], ],
// History // History
Row(
children: [
Container(
width: 4,
height: 20,
decoration: BoxDecoration(
color: colors.outlineVariant,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 10),
Text( Text(
'Pass Slip History', 'Pass Slip History',
style: theme.textTheme.titleMedium?.copyWith( style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
), ),
],
),
const SizedBox(height: 8), const SizedBox(height: 8),
for (int i = 0; i < historySlipList.length; i++) for (int i = 0; i < historySlipList.length; i++)
M3FadeSlideIn( M3FadeSlideIn(
@@ -5739,13 +5787,23 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> {
if (slips.isEmpty) if (slips.isEmpty)
Center( Center(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 24), padding: const EdgeInsets.symmetric(vertical: 32),
child: Text( 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.', 'No pass slip records.',
style: theme.textTheme.bodyMedium?.copyWith( style: theme.textTheme.bodyMedium?.copyWith(
color: colors.onSurfaceVariant, color: colors.onSurfaceVariant,
), ),
), ),
],
),
), ),
), ),
], ],
@@ -5769,20 +5827,25 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> {
final name = p?.fullName ?? slip.userId; final name = p?.fullName ?? slip.userId;
Color statusColor; Color statusColor;
IconData statusIcon;
switch (slip.status) { switch (slip.status) {
case 'approved': case 'approved':
statusColor = Colors.green; statusColor = Colors.green;
statusIcon = Icons.check_circle;
case 'rejected': case 'rejected':
statusColor = colors.error; statusColor = colors.error;
statusIcon = Icons.cancel;
case 'completed': case 'completed':
statusColor = colors.primary; statusColor = colors.primary;
statusIcon = Icons.task_alt;
default: default:
statusColor = Colors.orange; statusColor = Colors.orange;
statusIcon = Icons.hourglass_top;
} }
return Padding( return Padding(
padding: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.only(bottom: 8),
child: Card( child: M3Card.outlined(
child: Padding( child: Padding(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
child: Column( child: Column(
@@ -5790,69 +5853,97 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> {
children: [ children: [
Row( Row(
children: [ children: [
Expanded(child: Text(name, style: theme.textTheme.titleSmall)), Expanded(
child: Text(
name,
style: theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600),
),
),
if (isPending) ...[ if (isPending) ...[
SyncPendingBadge(isPending: true, compact: true), SyncPendingBadge(isPending: true, compact: true),
const SizedBox(width: 8), const SizedBox(width: 8),
], ],
Container( Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
horizontal: 10,
vertical: 4,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: statusColor.withValues(alpha: 0.15), color: statusColor.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(20),
), ),
child: Text( child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(statusIcon, size: 12, color: statusColor),
const SizedBox(width: 4),
Text(
slip.status.toUpperCase(), slip.status.toUpperCase(),
style: theme.textTheme.labelSmall?.copyWith( style: theme.textTheme.labelSmall?.copyWith(
color: statusColor, color: statusColor,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
), ),
],
),
), ),
], ],
), ),
const SizedBox(height: 4), const SizedBox(height: 6),
Text(slip.reason, style: theme.textTheme.bodyMedium), 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( Text(
'Requested: ${AppTime.formatDate(slip.requestedAt)} ${AppTime.formatTime(slip.requestedAt)}', 'Requested: ${AppTime.formatDate(slip.requestedAt)} ${AppTime.formatTime(slip.requestedAt)}',
style: theme.textTheme.bodySmall?.copyWith( style: theme.textTheme.bodySmall?.copyWith(color: colors.onSurfaceVariant),
color: colors.onSurfaceVariant,
), ),
],
), ),
if (slip.requestedStart != null) if (slip.requestedStart != null) ...[
const SizedBox(height: 2),
Row(
children: [
Icon(Icons.alarm, size: 13, color: colors.onSurfaceVariant),
const SizedBox(width: 4),
Text( Text(
'Preferred start: ${AppTime.formatTime(slip.requestedStart!)}', 'Preferred start: ${AppTime.formatTime(slip.requestedStart!)}',
style: theme.textTheme.bodySmall?.copyWith( style: theme.textTheme.bodySmall?.copyWith(color: colors.onSurfaceVariant),
color: colors.onSurfaceVariant,
), ),
],
), ),
if (slip.slipStart != null) ],
if (slip.slipStart != null) ...[
const SizedBox(height: 2),
Row(
children: [
Icon(Icons.directions_walk, size: 13, color: colors.onSurfaceVariant),
const SizedBox(width: 4),
Text( Text(
'Started: ${AppTime.formatTime(slip.slipStart!)}' 'Started: ${AppTime.formatTime(slip.slipStart!)}'
'${slip.slipEnd != null ? " · Ended: ${AppTime.formatTime(slip.slipEnd!)}" : ""}', '${slip.slipEnd != null ? " · Ended: ${AppTime.formatTime(slip.slipEnd!)}" : ""}',
style: theme.textTheme.bodySmall?.copyWith( style: theme.textTheme.bodySmall?.copyWith(color: colors.onSurfaceVariant),
color: colors.onSurfaceVariant,
), ),
],
), ),
],
if (showActions && slip.status == 'pending') ...[ if (showActions && slip.status == 'pending') ...[
const SizedBox(height: 8), const SizedBox(height: 10),
Row( Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
TextButton( OutlinedButton.icon(
onPressed: _submitting ? null : () => _rejectSlip(slip.id), onPressed: _submitting ? null : () => _rejectSlip(slip.id),
child: Text( icon: Icon(Icons.close, size: 16, color: colors.error),
'Reject', label: Text('Reject', style: TextStyle(color: colors.error)),
style: TextStyle(color: colors.error), style: OutlinedButton.styleFrom(
side: BorderSide(color: colors.error.withValues(alpha: 0.5)),
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
FilledButton( FilledButton.icon(
onPressed: _submitting ? null : () => _approveSlip(slip.id), onPressed: _submitting ? null : () => _approveSlip(slip.id),
child: const Text('Approve'), icon: const Icon(Icons.check, size: 16),
label: const Text('Approve'),
), ),
], ],
), ),
@@ -5976,22 +6067,41 @@ class _LeaveTabState extends ConsumerState<_LeaveTab> {
children: [ children: [
// Pending Approvals (admin only) // Pending Approvals (admin only)
if (isAdmin) ...[ if (isAdmin) ...[
Row(
children: [
Container(
width: 4,
height: 20,
decoration: BoxDecoration(
color: colors.tertiary,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 10),
Text( Text(
'Pending Approvals', 'Pending Approvals',
style: theme.textTheme.titleMedium?.copyWith( style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
), ),
],
),
const SizedBox(height: 8), const SizedBox(height: 8),
if (pendingApprovals.isEmpty) if (pendingApprovals.isEmpty)
Padding( Padding(
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(vertical: 12),
child: Text( child: Row(
children: [
Icon(Icons.inbox_outlined, size: 20, color: colors.onSurfaceVariant),
const SizedBox(width: 8),
Text(
'No pending leave requests.', 'No pending leave requests.',
style: theme.textTheme.bodyMedium?.copyWith( style: theme.textTheme.bodyMedium?.copyWith(
color: colors.onSurfaceVariant, color: colors.onSurfaceVariant,
), ),
), ),
],
),
), ),
for (int i = 0; i < pendingApprovals.length; i++) for (int i = 0; i < pendingApprovals.length; i++)
M3FadeSlideIn( M3FadeSlideIn(
@@ -6008,23 +6118,46 @@ class _LeaveTabState extends ConsumerState<_LeaveTab> {
], ],
// My Leave Applications // My Leave Applications
Row(
children: [
Container(
width: 4,
height: 20,
decoration: BoxDecoration(
color: colors.secondary,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 10),
Text( Text(
'My Leave Applications', 'My Leave Applications',
style: theme.textTheme.titleMedium?.copyWith( style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
), ),
],
),
const SizedBox(height: 8), const SizedBox(height: 8),
if (myLeaves.isEmpty) if (myLeaves.isEmpty)
Center( Center(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 24), padding: const EdgeInsets.symmetric(vertical: 32),
child: Text( 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.', 'You have no leave applications.',
style: theme.textTheme.bodyMedium?.copyWith( style: theme.textTheme.bodyMedium?.copyWith(
color: colors.onSurfaceVariant, color: colors.onSurfaceVariant,
), ),
), ),
],
),
), ),
), ),
for (int i = 0; i < myLeavesList.length; i++) for (int i = 0; i < myLeavesList.length; i++)
@@ -6042,12 +6175,25 @@ class _LeaveTabState extends ConsumerState<_LeaveTab> {
// All Leave History (admin only) // All Leave History (admin only)
if (isAdmin) ...[ if (isAdmin) ...[
const SizedBox(height: 24), const SizedBox(height: 24),
Row(
children: [
Container(
width: 4,
height: 20,
decoration: BoxDecoration(
color: colors.outlineVariant,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 10),
Text( Text(
'All Leave History', 'All Leave History',
style: theme.textTheme.titleMedium?.copyWith( style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
), ),
],
),
const SizedBox(height: 8), const SizedBox(height: 8),
for (int i = 0; i < allLeaveHistory.length; i++) for (int i = 0; i < allLeaveHistory.length; i++)
M3FadeSlideIn( M3FadeSlideIn(
@@ -6062,13 +6208,19 @@ class _LeaveTabState extends ConsumerState<_LeaveTab> {
), ),
if (allLeaveHistory.isEmpty) if (allLeaveHistory.isEmpty)
Padding( Padding(
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(vertical: 12),
child: Text( child: Row(
children: [
Icon(Icons.history_outlined, size: 20, color: colors.onSurfaceVariant),
const SizedBox(width: 8),
Text(
'No leave history from other staff.', 'No leave history from other staff.',
style: theme.textTheme.bodyMedium?.copyWith( style: theme.textTheme.bodyMedium?.copyWith(
color: colors.onSurfaceVariant, color: colors.onSurfaceVariant,
), ),
), ),
],
),
), ),
], ],
const SizedBox(height: 80), const SizedBox(height: 80),
@@ -6093,20 +6245,35 @@ class _LeaveTabState extends ConsumerState<_LeaveTab> {
final name = p?.fullName ?? leave.userId; final name = p?.fullName ?? leave.userId;
Color statusColor; Color statusColor;
IconData statusIcon;
switch (leave.status) { switch (leave.status) {
case 'approved': case 'approved':
statusColor = Colors.teal; statusColor = Colors.teal;
statusIcon = Icons.check_circle;
case 'rejected': case 'rejected':
statusColor = colors.error; statusColor = colors.error;
statusIcon = Icons.cancel;
case 'cancelled': case 'cancelled':
statusColor = colors.onSurfaceVariant; statusColor = colors.onSurfaceVariant;
statusIcon = Icons.remove_circle_outline;
default: default:
statusColor = Colors.orange; 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( return Padding(
padding: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.only(bottom: 8),
child: Card( child: M3Card.outlined(
child: Padding( child: Padding(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
child: Column( child: Column(
@@ -6114,81 +6281,113 @@ class _LeaveTabState extends ConsumerState<_LeaveTab> {
children: [ children: [
Row( Row(
children: [ children: [
Expanded(child: Text(name, style: theme.textTheme.titleSmall)), Expanded(
child: Text(
name,
style: theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600),
),
),
if (isPending) ...[ if (isPending) ...[
SyncPendingBadge(isPending: true, compact: true), SyncPendingBadge(isPending: true, compact: true),
const SizedBox(width: 8), const SizedBox(width: 8),
], ],
Container( Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
horizontal: 10,
vertical: 4,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: statusColor.withValues(alpha: 0.15), color: statusColor.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(20),
), ),
child: Text( child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(statusIcon, size: 12, color: statusColor),
const SizedBox(width: 4),
Text(
leave.status.toUpperCase(), leave.status.toUpperCase(),
style: theme.textTheme.labelSmall?.copyWith( style: theme.textTheme.labelSmall?.copyWith(
color: statusColor, color: statusColor,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
), ),
],
),
), ),
], ],
), ),
const SizedBox(height: 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( Text(
leave.leaveTypeLabel, leave.leaveTypeLabel,
style: theme.textTheme.bodyMedium?.copyWith( style: theme.textTheme.labelSmall?.copyWith(
fontWeight: FontWeight.w500, color: typeColor,
fontWeight: FontWeight.w600,
), ),
), ),
],
),
),
const SizedBox(height: 6),
Text(leave.justification, style: theme.textTheme.bodyMedium), Text(leave.justification, style: theme.textTheme.bodyMedium),
Text( 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.formatDate(leave.startTime)} '
'${AppTime.formatTime(leave.startTime)} ' '${AppTime.formatTime(leave.startTime)} '
'${AppTime.formatTime(leave.endTime)}', '${AppTime.formatTime(leave.endTime)}',
style: theme.textTheme.bodySmall?.copyWith( style: theme.textTheme.bodySmall?.copyWith(color: colors.onSurfaceVariant),
color: colors.onSurfaceVariant,
), ),
), ),
],
),
// Approve / Reject for admins on pending leaves // Approve / Reject for admins on pending leaves
if (showApproval && leave.status == 'pending') ...[ if (showApproval && leave.status == 'pending') ...[
const SizedBox(height: 8), const SizedBox(height: 10),
Row( Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
TextButton( OutlinedButton.icon(
onPressed: _submitting onPressed: _submitting ? null : () => _rejectLeave(leave.id),
? null icon: Icon(Icons.close, size: 16, color: colors.error),
: () => _rejectLeave(leave.id), label: Text('Reject', style: TextStyle(color: colors.error)),
child: Text( style: OutlinedButton.styleFrom(
'Reject', side: BorderSide(color: colors.error.withValues(alpha: 0.5)),
style: TextStyle(color: colors.error),
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
FilledButton( FilledButton.icon(
onPressed: _submitting onPressed: _submitting ? null : () => _approveLeave(leave.id),
? null icon: const Icon(Icons.check, size: 16),
: () => _approveLeave(leave.id), label: const Text('Approve'),
child: const Text('Approve'),
), ),
], ],
), ),
], ],
// Cancel future approved leaves: // Cancel future approved leaves
// - user can cancel own
// - admin can cancel anyone
if (!showApproval && _canCancelFutureApproved(leave)) ...[ if (!showApproval && _canCancelFutureApproved(leave)) ...[
const SizedBox(height: 8), const SizedBox(height: 8),
Align( Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: TextButton( child: OutlinedButton.icon(
onPressed: _submitting ? null : () => _cancelLeave(leave.id), onPressed: _submitting ? null : () => _cancelLeave(leave.id),
child: Text('Cancel', style: TextStyle(color: colors.error)), 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)),
),
), ),
), ),
], ],
@@ -6395,12 +6594,26 @@ class _PassSlipDialogState extends ConsumerState<_PassSlipDialog> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final colors = theme.colorScheme;
final content = Column( final content = Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('Request Pass Slip', style: theme.textTheme.headlineSmall), Row(
const SizedBox(height: 16), 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( Row(
children: [ children: [
Expanded( Expanded(
@@ -6433,8 +6646,7 @@ class _PassSlipDialogState extends ConsumerState<_PassSlipDialog> {
], ],
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
InkWell( M3Card.outlined(
borderRadius: BorderRadius.circular(12),
onTap: _submitting onTap: _submitting
? null ? null
: () async { : () async {
@@ -6446,33 +6658,44 @@ class _PassSlipDialogState extends ConsumerState<_PassSlipDialog> {
setState(() => _requestedStartTime = picked); setState(() => _requestedStartTime = picked);
} }
}, },
child: InputDecorator( child: Padding(
decoration: InputDecoration( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
labelText: 'Preferred start time (optional)', child: Row(
border: OutlineInputBorder( children: [
borderRadius: BorderRadius.circular(12), 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,
), ),
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( const SizedBox(height: 2),
Text(
_requestedStartTime != null _requestedStartTime != null
? _requestedStartTime!.format(context) ? _requestedStartTime!.format(context)
: 'Immediately upon approval', : 'Immediately upon approval',
style: theme.textTheme.bodyLarge?.copyWith( style: theme.textTheme.bodyMedium?.copyWith(
color: _requestedStartTime != null color: _requestedStartTime != null ? null : colors.onSurfaceVariant,
? null
: theme.colorScheme.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,
),
],
),
), ),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
@@ -6484,15 +6707,16 @@ class _PassSlipDialogState extends ConsumerState<_PassSlipDialog> {
child: const Text('Cancel'), child: const Text('Cancel'),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
FilledButton( FilledButton.icon(
onPressed: _submitting ? null : _submit, onPressed: _submitting ? null : _submit,
child: _submitting icon: _submitting
? const SizedBox( ? const SizedBox(
width: 16, width: 16,
height: 16, height: 16,
child: CircularProgressIndicator(strokeWidth: 2), 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 theme = Theme.of(context);
final colors = theme.colorScheme; 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( final content = Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('File Leave of Absence', style: theme.textTheme.headlineSmall),
const SizedBox(height: 16),
// Leave type
DropdownButtonFormField<String>(
// 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'),
onTap: _pickDate,
),
// Time range
Row( Row(
children: [ children: [
CircleAvatar(
radius: 22,
backgroundColor: colors.secondaryContainer,
child: Icon(Icons.event_busy, color: colors.onSecondaryContainer, size: 22),
),
const SizedBox(width: 14),
Expanded( Expanded(
child: ListTile( child: Text('File Leave of Absence', style: theme.textTheme.headlineSmall),
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.access_time),
title: Text(_startTime == null ? 'Start Time' : _startTime!.format(context)),
onTap: _pickStartTime,
), ),
],
), ),
const Padding( const SizedBox(height: 20),
padding: EdgeInsets.symmetric(horizontal: 8),
child: Icon(Icons.arrow_forward), // 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),
// 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( Expanded(
child: ListTile( child: Column(
contentPadding: EdgeInsets.zero, crossAxisAlignment: CrossAxisAlignment.start,
leading: const Icon(Icons.access_time), children: [
title: Text(_endTime == null ? 'End Time' : _endTime!.format(context)), Text(
subtitle: const Text('From shift schedule'), 'Date',
onTap: _pickEndTime, 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,
), ),
), ),
], ],
), ),
const SizedBox(height: 12), ),
Icon(Icons.chevron_right, size: 18, color: colors.onSurfaceVariant),
],
),
),
),
const SizedBox(height: 10),
// Time range buttons
Row(
children: [
Expanded(
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),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Icon(Icons.arrow_forward, size: 16, color: colors.onSurfaceVariant),
),
Expanded(
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: 16),
// Justification with AI // Justification with AI
Row( Row(
@@ -6779,14 +7071,27 @@ class _FileLeaveDialogState extends ConsumerState<_FileLeaveDialog> {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
if (widget.isAdmin) if (widget.isAdmin) ...[
Padding( M3Card.filled(
padding: const EdgeInsets.only(bottom: 8), 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( child: Text(
'As admin, your leave will be auto-approved.', 'Your leave will be auto-approved.',
style: theme.textTheme.bodySmall?.copyWith(color: colors.primary), style: theme.textTheme.bodySmall?.copyWith(color: colors.onPrimaryContainer),
), ),
), ),
],
),
),
),
const SizedBox(height: 12),
],
// Actions // Actions
Row( Row(