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/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)
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,
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,12 +5702,25 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> {
// Pending slips for admin approval
if (isAdmin) ...[
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++)
M3FadeSlideIn(
@@ -5706,24 +5735,43 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> {
),
if (pendingSlipList.isEmpty)
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Text(
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
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++)
M3FadeSlideIn(
@@ -5739,13 +5787,23 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> {
if (slips.isEmpty)
Center(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 24),
child: Text(
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,20 +5827,25 @@ 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(
@@ -5790,69 +5853,97 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> {
children: [
Row(
children: [
Expanded(child: Text(name, style: theme.textTheme.titleSmall)),
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: 10,
vertical: 4,
),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: statusColor.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(12),
color: statusColor.withValues(alpha: 0.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(),
style: theme.textTheme.labelSmall?.copyWith(
color: statusColor,
fontWeight: FontWeight.w600,
),
),
],
),
),
],
),
const SizedBox(height: 4),
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,
style: theme.textTheme.bodySmall?.copyWith(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(
'Preferred start: ${AppTime.formatTime(slip.requestedStart!)}',
style: theme.textTheme.bodySmall?.copyWith(
color: colors.onSurfaceVariant,
style: theme.textTheme.bodySmall?.copyWith(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(
'Started: ${AppTime.formatTime(slip.slipStart!)}'
'${slip.slipEnd != null ? " · Ended: ${AppTime.formatTime(slip.slipEnd!)}" : ""}',
style: theme.textTheme.bodySmall?.copyWith(
color: colors.onSurfaceVariant,
style: theme.textTheme.bodySmall?.copyWith(color: colors.onSurfaceVariant),
),
],
),
],
if (showActions && slip.status == 'pending') ...[
const SizedBox(height: 8),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
OutlinedButton.icon(
onPressed: _submitting ? null : () => _rejectSlip(slip.id),
child: Text(
'Reject',
style: TextStyle(color: colors.error),
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(
FilledButton.icon(
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: [
// Pending Approvals (admin only)
if (isAdmin) ...[
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(
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++)
M3FadeSlideIn(
@@ -6008,23 +6118,46 @@ class _LeaveTabState extends ConsumerState<_LeaveTab> {
],
// My Leave Applications
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(
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,
),
),
],
),
),
),
for (int i = 0; i < myLeavesList.length; i++)
@@ -6042,12 +6175,25 @@ class _LeaveTabState extends ConsumerState<_LeaveTab> {
// All Leave History (admin only)
if (isAdmin) ...[
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(
'All Leave History',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: 8),
for (int i = 0; i < allLeaveHistory.length; i++)
M3FadeSlideIn(
@@ -6062,13 +6208,19 @@ class _LeaveTabState extends ConsumerState<_LeaveTab> {
),
if (allLeaveHistory.isEmpty)
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Text(
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,
),
),
],
),
),
],
const SizedBox(height: 80),
@@ -6093,20 +6245,35 @@ 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: M3Card.outlined(
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
@@ -6114,81 +6281,113 @@ class _LeaveTabState extends ConsumerState<_LeaveTab> {
children: [
Row(
children: [
Expanded(child: Text(name, style: theme.textTheme.titleSmall)),
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: 10,
vertical: 4,
),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: statusColor.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(12),
color: statusColor.withValues(alpha: 0.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(),
style: theme.textTheme.labelSmall?.copyWith(
color: statusColor,
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(
leave.leaveTypeLabel,
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
style: theme.textTheme.labelSmall?.copyWith(
color: typeColor,
fontWeight: FontWeight.w600,
),
),
],
),
),
const SizedBox(height: 6),
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.formatTime(leave.startTime)} '
'${AppTime.formatTime(leave.endTime)}',
style: theme.textTheme.bodySmall?.copyWith(
color: colors.onSurfaceVariant,
style: theme.textTheme.bodySmall?.copyWith(color: colors.onSurfaceVariant),
),
),
],
),
// Approve / Reject for admins on pending leaves
if (showApproval && leave.status == 'pending') ...[
const SizedBox(height: 8),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: _submitting
? null
: () => _rejectLeave(leave.id),
child: Text(
'Reject',
style: TextStyle(color: colors.error),
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(
onPressed: _submitting
? null
: () => _approveLeave(leave.id),
child: const Text('Approve'),
FilledButton.icon(
onPressed: _submitting ? null : () => _approveLeave(leave.id),
icon: const Icon(Icons.check, size: 16),
label: const Text('Approve'),
),
],
),
],
// Cancel future approved leaves:
// - user can cancel own
// - admin can cancel anyone
// Cancel future approved leaves
if (!showApproval && _canCancelFutureApproved(leave)) ...[
const SizedBox(height: 8),
Align(
alignment: Alignment.centerRight,
child: TextButton(
child: OutlinedButton.icon(
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) {
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,33 +6658,44 @@ class _PassSlipDialogState extends ConsumerState<_PassSlipDialog> {
setState(() => _requestedStartTime = picked);
}
},
child: InputDecorator(
decoration: InputDecoration(
labelText: 'Preferred start time (optional)',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
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,
),
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!.format(context)
: 'Immediately upon approval',
style: theme.textTheme.bodyLarge?.copyWith(
color: _requestedStartTime != null
? null
: theme.colorScheme.onSurfaceVariant,
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,
),
],
),
),
),
const SizedBox(height: 24),
@@ -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),
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(
children: [
CircleAvatar(
radius: 22,
backgroundColor: colors.secondaryContainer,
child: Icon(Icons.event_busy, color: colors.onSecondaryContainer, size: 22),
),
const SizedBox(width: 14),
Expanded(
child: ListTile(
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.access_time),
title: Text(_startTime == null ? 'Start Time' : _startTime!.format(context)),
onTap: _pickStartTime,
child: Text('File Leave of Absence', style: theme.textTheme.headlineSmall),
),
],
),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 8),
child: Icon(Icons.arrow_forward),
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),
// 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: 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: 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,
),
),
],
),
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
Row(
@@ -6779,14 +7071,27 @@ class _FileLeaveDialogState extends ConsumerState<_FileLeaveDialog> {
),
const SizedBox(height: 16),
if (widget.isAdmin)
Padding(
padding: const EdgeInsets.only(bottom: 8),
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(
'As admin, your leave will be auto-approved.',
style: theme.textTheme.bodySmall?.copyWith(color: colors.primary),
'Your leave will be auto-approved.',
style: theme.textTheme.bodySmall?.copyWith(color: colors.onPrimaryContainer),
),
),
],
),
),
),
const SizedBox(height: 12),
],
// Actions
Row(