* Task/Ticket desktop dialogs — widened from 520 → 600dp, and tasks now uses SizedBox(width: 600) instead of ConstrainedBox(maxWidth:) which was the root cause of the "still small" issue (max-only never forced expansion)
* Pass Slip + Leave — both dialogs gained an isSheet flag; callers now branch on AppBreakpoints.tablet: bottom sheet on mobile, dialog on desktop
This commit is contained in:
@@ -39,6 +39,7 @@ import '../../utils/snackbar.dart';
|
||||
import '../../widgets/gemini_animated_text_field.dart';
|
||||
import '../../widgets/gemini_button.dart';
|
||||
import '../../widgets/multi_select_picker.dart';
|
||||
import '../../widgets/app_breakpoints.dart';
|
||||
import '../../widgets/app_page_header.dart';
|
||||
import '../../widgets/responsive_body.dart';
|
||||
import '../../widgets/sync_pending_badge.dart';
|
||||
@@ -150,65 +151,74 @@ class _AttendanceScreenState extends ConsumerState<AttendanceScreen>
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
// Leave option
|
||||
if (canFileLeave) ...[
|
||||
return SafeArea(
|
||||
bottom: true,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
// Leave option
|
||||
if (canFileLeave) ...[
|
||||
_FabMenuItem(
|
||||
heroTag: 'fab_leave',
|
||||
label: 'File Leave',
|
||||
icon: Icons.event_busy,
|
||||
color: colors.tertiaryContainer,
|
||||
onColor: colors.onTertiaryContainer,
|
||||
onTap: () {
|
||||
setState(() => _fabMenuOpen = false);
|
||||
_showLeaveDialog(context, isAdmin);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
// Pass Slip option
|
||||
_FabMenuItem(
|
||||
heroTag: 'fab_leave',
|
||||
label: 'File Leave',
|
||||
icon: Icons.event_busy,
|
||||
color: colors.tertiaryContainer,
|
||||
onColor: colors.onTertiaryContainer,
|
||||
heroTag: 'fab_slip',
|
||||
label: 'Request Slip',
|
||||
icon: Icons.receipt_long,
|
||||
color: colors.secondaryContainer,
|
||||
onColor: colors.onSecondaryContainer,
|
||||
onTap: () {
|
||||
setState(() => _fabMenuOpen = false);
|
||||
_showLeaveDialog(context, isAdmin);
|
||||
_showPassSlipDialog(context, profile);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// Close button
|
||||
M3Fab(
|
||||
heroTag: 'fab_close',
|
||||
onPressed: () => setState(() => _fabMenuOpen = false),
|
||||
icon: const Icon(Icons.close),
|
||||
),
|
||||
],
|
||||
// Pass Slip option
|
||||
_FabMenuItem(
|
||||
heroTag: 'fab_slip',
|
||||
label: 'Request Slip',
|
||||
icon: Icons.receipt_long,
|
||||
color: colors.secondaryContainer,
|
||||
onColor: colors.onSecondaryContainer,
|
||||
onTap: () {
|
||||
setState(() => _fabMenuOpen = false);
|
||||
_showPassSlipDialog(context, profile);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// Close button
|
||||
M3Fab(
|
||||
heroTag: 'fab_close',
|
||||
onPressed: () => setState(() => _fabMenuOpen = false),
|
||||
icon: const Icon(Icons.close),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showLeaveDialog(BuildContext context, bool isAdmin) {
|
||||
m3ShowDialog(
|
||||
context: context,
|
||||
builder: (ctx) => _FileLeaveDialog(
|
||||
isAdmin: isAdmin,
|
||||
onSubmitted: () {
|
||||
if (mounted) {
|
||||
showSuccessSnackBar(
|
||||
context,
|
||||
isAdmin
|
||||
? 'Leave filed and auto-approved.'
|
||||
: 'Leave application submitted for approval.',
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
final isMobile = MediaQuery.sizeOf(context).width < AppBreakpoints.tablet;
|
||||
void onSubmitted() {
|
||||
if (mounted) {
|
||||
showSuccessSnackBar(
|
||||
context,
|
||||
isAdmin ? 'Leave filed and auto-approved.' : 'Leave application submitted for approval.',
|
||||
);
|
||||
}
|
||||
}
|
||||
if (isMobile) {
|
||||
m3ShowBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useSafeArea: true,
|
||||
builder: (_) => _FileLeaveDialog(isAdmin: isAdmin, onSubmitted: onSubmitted, isSheet: true),
|
||||
);
|
||||
} else {
|
||||
m3ShowDialog(
|
||||
context: context,
|
||||
builder: (_) => _FileLeaveDialog(isAdmin: isAdmin, onSubmitted: onSubmitted),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _showPassSlipDialog(BuildContext context, Profile profile) {
|
||||
@@ -243,17 +253,24 @@ class _AttendanceScreenState extends ConsumerState<AttendanceScreen>
|
||||
return;
|
||||
}
|
||||
|
||||
m3ShowDialog(
|
||||
context: context,
|
||||
builder: (ctx) => _PassSlipDialog(
|
||||
scheduleId: todaySchedule.first.id,
|
||||
onSubmitted: () {
|
||||
if (mounted) {
|
||||
showSuccessSnackBar(context, 'Pass slip requested.');
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
void onSubmitted() {
|
||||
if (mounted) showSuccessSnackBar(context, 'Pass slip requested.');
|
||||
}
|
||||
|
||||
final isMobile = MediaQuery.sizeOf(context).width < AppBreakpoints.tablet;
|
||||
if (isMobile) {
|
||||
m3ShowBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useSafeArea: true,
|
||||
builder: (_) => _PassSlipDialog(scheduleId: todaySchedule.first.id, onSubmitted: onSubmitted, isSheet: true),
|
||||
);
|
||||
} else {
|
||||
m3ShowDialog(
|
||||
context: context,
|
||||
builder: (_) => _PassSlipDialog(scheduleId: todaySchedule.first.id, onSubmitted: onSubmitted),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2195,17 +2212,21 @@ class _LogbookTabState extends ConsumerState<_LogbookTab> {
|
||||
}) {
|
||||
final groupedByDate = _groupByDate(entries);
|
||||
|
||||
final groups = groupedByDate.entries.toList();
|
||||
return ListView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
children: groupedByDate.entries.map((group) {
|
||||
final shiftGroups = _groupByShift(group.value);
|
||||
return _DateGroupTile(
|
||||
dateLabel: group.key,
|
||||
shiftGroups: shiftGroups,
|
||||
currentUserId: currentUserId,
|
||||
onReverify: onReverify,
|
||||
);
|
||||
}).toList(),
|
||||
children: [
|
||||
for (int i = 0; i < groups.length; i++)
|
||||
M3FadeSlideIn(
|
||||
delay: Duration(milliseconds: i.clamp(0, 6) * 60),
|
||||
child: _DateGroupTile(
|
||||
dateLabel: groups[i].key,
|
||||
shiftGroups: _groupByShift(groups[i].value),
|
||||
currentUserId: currentUserId,
|
||||
onReverify: onReverify,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2463,10 +2484,9 @@ class _DateGroupTile extends StatelessWidget {
|
||||
final isMobile = MediaQuery.sizeOf(context).width < 700;
|
||||
|
||||
if (isMobile) {
|
||||
showModalBottomSheet<void>(
|
||||
m3ShowBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
showDragHandle: true,
|
||||
useSafeArea: true,
|
||||
builder: (ctx) => DraggableScrollableSheet(
|
||||
initialChildSize: 0.9,
|
||||
@@ -3558,7 +3578,9 @@ class _MyScheduleTabState extends ConsumerState<_MyScheduleTab> {
|
||||
s.requesterId == currentUserId &&
|
||||
s.status == 'pending');
|
||||
|
||||
return Padding(
|
||||
return M3FadeSlideIn(
|
||||
delay: Duration(milliseconds: index.clamp(0, 6) * 60),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Card(
|
||||
child: Padding(
|
||||
@@ -3663,6 +3685,7 @@ class _MyScheduleTabState extends ConsumerState<_MyScheduleTab> {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -4111,7 +4134,7 @@ class _MyScheduleTabState extends ConsumerState<_MyScheduleTab> {
|
||||
) {
|
||||
final isMobile = MediaQuery.sizeOf(context).width < 600;
|
||||
if (isMobile) {
|
||||
showModalBottomSheet(
|
||||
m3ShowBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useSafeArea: true,
|
||||
@@ -4904,6 +4927,8 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> {
|
||||
|
||||
return slipsAsync.when(
|
||||
data: (slips) {
|
||||
final pendingSlipList = slips.where((s) => s.status == 'pending').toList();
|
||||
final historySlipList = slips.where((s) => s.status != 'pending' || !isAdmin).take(50).toList();
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
@@ -4997,18 +5022,18 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...slips
|
||||
.where((s) => s.status == 'pending')
|
||||
.map(
|
||||
(slip) => _buildSlipCard(
|
||||
context,
|
||||
slip,
|
||||
profileById,
|
||||
showActions: true,
|
||||
isPending: pendingSlips.any((p) => p.id == slip.id),
|
||||
),
|
||||
for (int i = 0; i < pendingSlipList.length; i++)
|
||||
M3FadeSlideIn(
|
||||
delay: Duration(milliseconds: i.clamp(0, 6) * 60),
|
||||
child: _buildSlipCard(
|
||||
context,
|
||||
pendingSlipList[i],
|
||||
profileById,
|
||||
showActions: true,
|
||||
isPending: pendingSlips.any((p) => p.id == pendingSlipList[i].id),
|
||||
),
|
||||
if (slips.where((s) => s.status == 'pending').isEmpty)
|
||||
),
|
||||
if (pendingSlipList.isEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text(
|
||||
@@ -5029,18 +5054,17 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...slips
|
||||
.where((s) => s.status != 'pending' || !isAdmin)
|
||||
.take(50)
|
||||
.map(
|
||||
(slip) => _buildSlipCard(
|
||||
context,
|
||||
slip,
|
||||
profileById,
|
||||
showActions: false,
|
||||
isPending: pendingSlips.any((p) => p.id == slip.id),
|
||||
),
|
||||
for (int i = 0; i < historySlipList.length; i++)
|
||||
M3FadeSlideIn(
|
||||
delay: Duration(milliseconds: i.clamp(0, 6) * 60),
|
||||
child: _buildSlipCard(
|
||||
context,
|
||||
historySlipList[i],
|
||||
profileById,
|
||||
showActions: false,
|
||||
isPending: pendingSlips.any((p) => p.id == historySlipList[i].id),
|
||||
),
|
||||
),
|
||||
if (slips.isEmpty)
|
||||
Center(
|
||||
child: Padding(
|
||||
@@ -5271,6 +5295,11 @@ class _LeaveTabState extends ConsumerState<_LeaveTab> {
|
||||
.toList()
|
||||
: <LeaveOfAbsence>[];
|
||||
|
||||
final myLeavesList = myLeaves.take(50).toList();
|
||||
final allLeaveHistory = leaves
|
||||
.where((l) => l.status != 'pending' && l.userId != profile.id)
|
||||
.take(50)
|
||||
.toList();
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
@@ -5293,15 +5322,17 @@ class _LeaveTabState extends ConsumerState<_LeaveTab> {
|
||||
),
|
||||
),
|
||||
),
|
||||
...pendingApprovals.map(
|
||||
(leave) => _buildLeaveCard(
|
||||
context,
|
||||
leave,
|
||||
profileById,
|
||||
showApproval: true,
|
||||
isPending: pendingLeavesList.any((l) => l.id == leave.id),
|
||||
for (int i = 0; i < pendingApprovals.length; i++)
|
||||
M3FadeSlideIn(
|
||||
delay: Duration(milliseconds: i.clamp(0, 6) * 60),
|
||||
child: _buildLeaveCard(
|
||||
context,
|
||||
pendingApprovals[i],
|
||||
profileById,
|
||||
showApproval: true,
|
||||
isPending: pendingLeavesList.any((l) => l.id == pendingApprovals[i].id),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
|
||||
@@ -5325,17 +5356,17 @@ class _LeaveTabState extends ConsumerState<_LeaveTab> {
|
||||
),
|
||||
),
|
||||
),
|
||||
...myLeaves
|
||||
.take(50)
|
||||
.map(
|
||||
(leave) => _buildLeaveCard(
|
||||
context,
|
||||
leave,
|
||||
profileById,
|
||||
showApproval: false,
|
||||
isPending: pendingLeavesList.any((l) => l.id == leave.id),
|
||||
),
|
||||
for (int i = 0; i < myLeavesList.length; i++)
|
||||
M3FadeSlideIn(
|
||||
delay: Duration(milliseconds: i.clamp(0, 6) * 60),
|
||||
child: _buildLeaveCard(
|
||||
context,
|
||||
myLeavesList[i],
|
||||
profileById,
|
||||
showApproval: false,
|
||||
isPending: pendingLeavesList.any((l) => l.id == myLeavesList[i].id),
|
||||
),
|
||||
),
|
||||
|
||||
// ── All Leave History (admin only) ──
|
||||
if (isAdmin) ...[
|
||||
@@ -5347,21 +5378,18 @@ class _LeaveTabState extends ConsumerState<_LeaveTab> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...leaves
|
||||
.where((l) => l.status != 'pending' && l.userId != profile.id)
|
||||
.take(50)
|
||||
.map(
|
||||
(leave) => _buildLeaveCard(
|
||||
context,
|
||||
leave,
|
||||
profileById,
|
||||
showApproval: false,
|
||||
isPending: pendingLeavesList.any((l) => l.id == leave.id),
|
||||
),
|
||||
for (int i = 0; i < allLeaveHistory.length; i++)
|
||||
M3FadeSlideIn(
|
||||
delay: Duration(milliseconds: i.clamp(0, 6) * 60),
|
||||
child: _buildLeaveCard(
|
||||
context,
|
||||
allLeaveHistory[i],
|
||||
profileById,
|
||||
showApproval: false,
|
||||
isPending: pendingLeavesList.any((l) => l.id == allLeaveHistory[i].id),
|
||||
),
|
||||
if (leaves
|
||||
.where((l) => l.status != 'pending' && l.userId != profile.id)
|
||||
.isEmpty)
|
||||
),
|
||||
if (allLeaveHistory.isEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text(
|
||||
@@ -5629,9 +5657,10 @@ class _FabMenuItem extends StatelessWidget {
|
||||
|
||||
// ─── Pass Slip Dialog (with Gemini) ─────────────────────────────
|
||||
class _PassSlipDialog extends ConsumerStatefulWidget {
|
||||
const _PassSlipDialog({required this.scheduleId, required this.onSubmitted});
|
||||
const _PassSlipDialog({required this.scheduleId, required this.onSubmitted, this.isSheet = false});
|
||||
final String scheduleId;
|
||||
final VoidCallback onSubmitted;
|
||||
final bool isSheet;
|
||||
|
||||
@override
|
||||
ConsumerState<_PassSlipDialog> createState() => _PassSlipDialogState();
|
||||
@@ -5695,121 +5724,127 @@ class _PassSlipDialogState extends ConsumerState<_PassSlipDialog> {
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
final content = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Request Pass Slip', style: theme.textTheme.headlineSmall),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: GeminiAnimatedTextField(
|
||||
controller: _reasonController,
|
||||
labelText: 'Reason',
|
||||
maxLines: 3,
|
||||
enabled: !_submitting,
|
||||
isProcessing: _isGeminiProcessing,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: GeminiButton(
|
||||
textController: _reasonController,
|
||||
onTextUpdated: (text) {
|
||||
setState(() => _reasonController.text = text);
|
||||
},
|
||||
onProcessingStateChanged: (processing) {
|
||||
setState(() => _isGeminiProcessing = processing);
|
||||
},
|
||||
tooltip: 'Translate/Enhance with AI',
|
||||
promptBuilder: (_) =>
|
||||
'Translate this sentence to clear professional English '
|
||||
'if needed, and enhance grammar/clarity while preserving '
|
||||
'the original meaning. Return ONLY the improved text, '
|
||||
'with no explanations, no recommendations, and no extra context.',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
InkWell(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
onTap: _submitting
|
||||
? null
|
||||
: () async {
|
||||
final picked = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: _requestedStartTime ?? TimeOfDay.now(),
|
||||
);
|
||||
if (picked != null && mounted) {
|
||||
setState(() => _requestedStartTime = picked);
|
||||
}
|
||||
},
|
||||
child: InputDecorator(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Preferred start time (optional)',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
prefixIcon: const Icon(Icons.schedule),
|
||||
suffixIcon: _requestedStartTime != null
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: _submitting
|
||||
? null
|
||||
: () => setState(() => _requestedStartTime = null),
|
||||
tooltip: 'Clear',
|
||||
)
|
||||
: null,
|
||||
),
|
||||
child: Text(
|
||||
_requestedStartTime != null
|
||||
? _requestedStartTime!.format(context)
|
||||
: 'Immediately upon approval',
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: _requestedStartTime != null
|
||||
? null
|
||||
: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: _submitting ? null : () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
FilledButton(
|
||||
onPressed: _submitting ? null : _submit,
|
||||
child: _submitting
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text('Submit'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
if (widget.isSheet) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 24,
|
||||
right: 24,
|
||||
top: 8,
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom + 24,
|
||||
),
|
||||
child: SingleChildScrollView(child: content),
|
||||
);
|
||||
}
|
||||
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 480),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Request Pass Slip', style: theme.textTheme.headlineSmall),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: GeminiAnimatedTextField(
|
||||
controller: _reasonController,
|
||||
labelText: 'Reason',
|
||||
maxLines: 3,
|
||||
enabled: !_submitting,
|
||||
isProcessing: _isGeminiProcessing,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: GeminiButton(
|
||||
textController: _reasonController,
|
||||
onTextUpdated: (text) {
|
||||
setState(() => _reasonController.text = text);
|
||||
},
|
||||
onProcessingStateChanged: (processing) {
|
||||
setState(() => _isGeminiProcessing = processing);
|
||||
},
|
||||
tooltip: 'Translate/Enhance with AI',
|
||||
promptBuilder: (_) =>
|
||||
'Translate this sentence to clear professional English '
|
||||
'if needed, and enhance grammar/clarity while preserving '
|
||||
'the original meaning. Return ONLY the improved text, '
|
||||
'with no explanations, no recommendations, and no extra context.',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Optional start time picker
|
||||
InkWell(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
onTap: _submitting
|
||||
? null
|
||||
: () async {
|
||||
final picked = await showTimePicker(
|
||||
context: context,
|
||||
initialTime:
|
||||
_requestedStartTime ?? TimeOfDay.now(),
|
||||
);
|
||||
if (picked != null && mounted) {
|
||||
setState(() => _requestedStartTime = picked);
|
||||
}
|
||||
},
|
||||
child: InputDecorator(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Preferred start time (optional)',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
prefixIcon: const Icon(Icons.schedule),
|
||||
suffixIcon: _requestedStartTime != null
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: _submitting
|
||||
? null
|
||||
: () => setState(
|
||||
() => _requestedStartTime = null),
|
||||
tooltip: 'Clear',
|
||||
)
|
||||
: null,
|
||||
),
|
||||
child: Text(
|
||||
_requestedStartTime != null
|
||||
? _requestedStartTime!.format(context)
|
||||
: 'Immediately upon approval',
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: _requestedStartTime != null
|
||||
? null
|
||||
: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: _submitting
|
||||
? null
|
||||
: () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
FilledButton(
|
||||
onPressed: _submitting ? null : _submit,
|
||||
child: _submitting
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text('Submit'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Padding(padding: const EdgeInsets.all(24), child: content),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -5817,9 +5852,10 @@ class _PassSlipDialogState extends ConsumerState<_PassSlipDialog> {
|
||||
|
||||
// ─── File Leave Dialog ──────────────────────────────────────────
|
||||
class _FileLeaveDialog extends ConsumerStatefulWidget {
|
||||
const _FileLeaveDialog({required this.isAdmin, required this.onSubmitted});
|
||||
const _FileLeaveDialog({required this.isAdmin, required this.onSubmitted, this.isSheet = false});
|
||||
final bool isAdmin;
|
||||
final VoidCallback onSubmitted;
|
||||
final bool isSheet;
|
||||
|
||||
@override
|
||||
ConsumerState<_FileLeaveDialog> createState() => _FileLeaveDialogState();
|
||||
@@ -5978,165 +6014,153 @@ class _FileLeaveDialogState extends ConsumerState<_FileLeaveDialog> {
|
||||
final theme = Theme.of(context);
|
||||
final colors = theme.colorScheme;
|
||||
|
||||
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: [
|
||||
Expanded(
|
||||
child: ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(Icons.access_time),
|
||||
title: Text(_startTime == null ? 'Start Time' : _startTime!.format(context)),
|
||||
onTap: _pickStartTime,
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Icon(Icons.arrow_forward),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Justification with AI
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: GeminiAnimatedTextField(
|
||||
controller: _justificationController,
|
||||
labelText: 'Justification',
|
||||
maxLines: 3,
|
||||
enabled: !_submitting,
|
||||
isProcessing: _isGeminiProcessing,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: GeminiButton(
|
||||
textController: _justificationController,
|
||||
onTextUpdated: (text) {
|
||||
setState(() => _justificationController.text = text);
|
||||
},
|
||||
onProcessingStateChanged: (processing) {
|
||||
setState(() => _isGeminiProcessing = processing);
|
||||
},
|
||||
tooltip: 'Translate/Enhance with AI',
|
||||
promptBuilder: (_) =>
|
||||
'Translate this sentence to clear professional English '
|
||||
'if needed, and enhance grammar/clarity while preserving '
|
||||
'the original meaning. Return ONLY the improved text, '
|
||||
'with no explanations, no recommendations, and no extra context.',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
if (widget.isAdmin)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Text(
|
||||
'As admin, your leave will be auto-approved.',
|
||||
style: theme.textTheme.bodySmall?.copyWith(color: colors.primary),
|
||||
),
|
||||
),
|
||||
|
||||
// Actions
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: _submitting ? null : () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
FilledButton.icon(
|
||||
onPressed: _submitting ? null : _submit,
|
||||
icon: _submitting
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.event_busy),
|
||||
label: const Text('File Leave'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
if (widget.isSheet) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 24,
|
||||
right: 24,
|
||||
top: 8,
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom + 24,
|
||||
),
|
||||
child: SingleChildScrollView(child: content),
|
||||
);
|
||||
}
|
||||
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 480),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: SingleChildScrollView(
|
||||
child: 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: [
|
||||
Expanded(
|
||||
child: ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(Icons.access_time),
|
||||
title: Text(
|
||||
_startTime == null
|
||||
? 'Start Time'
|
||||
: _startTime!.format(context),
|
||||
),
|
||||
onTap: _pickStartTime,
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Icon(Icons.arrow_forward),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Justification with AI
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: GeminiAnimatedTextField(
|
||||
controller: _justificationController,
|
||||
labelText: 'Justification',
|
||||
maxLines: 3,
|
||||
enabled: !_submitting,
|
||||
isProcessing: _isGeminiProcessing,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: GeminiButton(
|
||||
textController: _justificationController,
|
||||
onTextUpdated: (text) {
|
||||
setState(() {
|
||||
_justificationController.text = text;
|
||||
});
|
||||
},
|
||||
onProcessingStateChanged: (processing) {
|
||||
setState(() => _isGeminiProcessing = processing);
|
||||
},
|
||||
tooltip: 'Translate/Enhance with AI',
|
||||
promptBuilder: (_) =>
|
||||
'Translate this sentence to clear professional English '
|
||||
'if needed, and enhance grammar/clarity while preserving '
|
||||
'the original meaning. Return ONLY the improved text, '
|
||||
'with no explanations, no recommendations, and no extra context.',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
if (widget.isAdmin)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Text(
|
||||
'As admin, your leave will be auto-approved.',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colors.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Actions
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: _submitting
|
||||
? null
|
||||
: () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
FilledButton.icon(
|
||||
onPressed: _submitting ? null : _submit,
|
||||
icon: _submitting
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.event_busy),
|
||||
label: const Text('File Leave'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: SingleChildScrollView(child: content),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user