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/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(
|
||||||
|
|||||||
Reference in New Issue
Block a user