Enhanced Leave and Pass Slip UI/UX
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user