* 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:
@@ -28,6 +28,7 @@ import '../../widgets/tasq_adaptive_list.dart';
|
||||
import '../../widgets/typing_dots.dart';
|
||||
import '../../theme/app_surfaces.dart';
|
||||
import '../../utils/snackbar.dart';
|
||||
import '../../widgets/app_breakpoints.dart';
|
||||
import '../../widgets/app_page_header.dart';
|
||||
import '../../widgets/app_state_view.dart';
|
||||
import '../../utils/subject_suggestions.dart';
|
||||
@@ -324,106 +325,114 @@ class _TasksListScreenState extends ConsumerState<TasksListScreen>
|
||||
latestAssigneeByTaskId: latestAssigneeByTaskId,
|
||||
);
|
||||
|
||||
final filterHeader = Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 220,
|
||||
child: TextField(
|
||||
controller: _subjectController,
|
||||
onChanged: (_) => setState(() {}),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Subject',
|
||||
prefixIcon: Icon(Icons.search),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 200,
|
||||
child: DropdownButtonFormField<String?>(
|
||||
isExpanded: true,
|
||||
key: ValueKey(_selectedOfficeId),
|
||||
initialValue: _selectedOfficeId,
|
||||
items: officeOptions,
|
||||
onChanged: (value) =>
|
||||
setState(() => _selectedOfficeId = value),
|
||||
decoration: const InputDecoration(labelText: 'Office'),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 160,
|
||||
child: TextField(
|
||||
controller: _taskNumberController,
|
||||
onChanged: (_) => setState(() {}),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Task #',
|
||||
prefixIcon: Icon(Icons.filter_alt),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_tabController.index == 1)
|
||||
SizedBox(
|
||||
width: 220,
|
||||
child: DropdownButtonFormField<String?>(
|
||||
isExpanded: true,
|
||||
key: ValueKey(_selectedAssigneeId),
|
||||
initialValue: _selectedAssigneeId,
|
||||
items: staffOptions,
|
||||
onChanged: (value) =>
|
||||
setState(() => _selectedAssigneeId = value),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Assigned staff',
|
||||
final filterHeader = LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final isMobile = constraints.maxWidth < 600;
|
||||
final halfWidth = (constraints.maxWidth - 12) / 2;
|
||||
return Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: isMobile ? constraints.maxWidth : 220,
|
||||
child: TextField(
|
||||
controller: _subjectController,
|
||||
onChanged: (_) => setState(() {}),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Subject',
|
||||
prefixIcon: Icon(Icons.search),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 180,
|
||||
child: DropdownButtonFormField<String?>(
|
||||
isExpanded: true,
|
||||
key: ValueKey(_selectedStatus),
|
||||
initialValue: _selectedStatus,
|
||||
items: statusOptions,
|
||||
onChanged: (value) =>
|
||||
setState(() => _selectedStatus = value),
|
||||
decoration: const InputDecoration(labelText: 'Status'),
|
||||
),
|
||||
),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () async {
|
||||
final next = await showDateRangePicker(
|
||||
context: context,
|
||||
firstDate: DateTime(2020),
|
||||
lastDate: AppTime.now().add(
|
||||
const Duration(days: 365),
|
||||
SizedBox(
|
||||
width: isMobile ? halfWidth : 200,
|
||||
child: DropdownButtonFormField<String?>(
|
||||
isExpanded: true,
|
||||
key: ValueKey(_selectedOfficeId),
|
||||
initialValue: _selectedOfficeId,
|
||||
items: officeOptions,
|
||||
onChanged: (value) =>
|
||||
setState(() => _selectedOfficeId = value),
|
||||
decoration:
|
||||
const InputDecoration(labelText: 'Office'),
|
||||
),
|
||||
currentDate: AppTime.now(),
|
||||
initialDateRange: _selectedDateRange,
|
||||
);
|
||||
if (!mounted) return;
|
||||
setState(() => _selectedDateRange = next);
|
||||
},
|
||||
icon: const Icon(Icons.date_range),
|
||||
label: Text(
|
||||
_selectedDateRange == null
|
||||
? 'Date range'
|
||||
: AppTime.formatDateRange(_selectedDateRange!),
|
||||
),
|
||||
),
|
||||
if (_hasTaskFilters)
|
||||
TextButton.icon(
|
||||
onPressed: () => setState(() {
|
||||
_subjectController.clear();
|
||||
_selectedOfficeId = null;
|
||||
_selectedStatus = null;
|
||||
_selectedAssigneeId = null;
|
||||
_selectedDateRange = null;
|
||||
}),
|
||||
icon: const Icon(Icons.close),
|
||||
label: const Text('Clear'),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
width: isMobile ? halfWidth : 160,
|
||||
child: TextField(
|
||||
controller: _taskNumberController,
|
||||
onChanged: (_) => setState(() {}),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Task #',
|
||||
prefixIcon: Icon(Icons.filter_alt),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_tabController.index == 1)
|
||||
SizedBox(
|
||||
width: isMobile ? halfWidth : 220,
|
||||
child: DropdownButtonFormField<String?>(
|
||||
isExpanded: true,
|
||||
key: ValueKey(_selectedAssigneeId),
|
||||
initialValue: _selectedAssigneeId,
|
||||
items: staffOptions,
|
||||
onChanged: (value) =>
|
||||
setState(() => _selectedAssigneeId = value),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Assigned staff',
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: isMobile ? halfWidth : 180,
|
||||
child: DropdownButtonFormField<String?>(
|
||||
isExpanded: true,
|
||||
key: ValueKey(_selectedStatus),
|
||||
initialValue: _selectedStatus,
|
||||
items: statusOptions,
|
||||
onChanged: (value) =>
|
||||
setState(() => _selectedStatus = value),
|
||||
decoration:
|
||||
const InputDecoration(labelText: 'Status'),
|
||||
),
|
||||
),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () async {
|
||||
final next = await showDateRangePicker(
|
||||
context: context,
|
||||
firstDate: DateTime(2020),
|
||||
lastDate: AppTime.now().add(
|
||||
const Duration(days: 365),
|
||||
),
|
||||
currentDate: AppTime.now(),
|
||||
initialDateRange: _selectedDateRange,
|
||||
);
|
||||
if (!mounted) return;
|
||||
setState(() => _selectedDateRange = next);
|
||||
},
|
||||
icon: const Icon(Icons.date_range),
|
||||
label: Text(
|
||||
_selectedDateRange == null
|
||||
? 'Date range'
|
||||
: AppTime.formatDateRange(_selectedDateRange!),
|
||||
),
|
||||
),
|
||||
if (_hasTaskFilters)
|
||||
TextButton.icon(
|
||||
onPressed: () => setState(() {
|
||||
_subjectController.clear();
|
||||
_selectedOfficeId = null;
|
||||
_selectedStatus = null;
|
||||
_selectedAssigneeId = null;
|
||||
_selectedDateRange = null;
|
||||
}),
|
||||
icon: const Icon(Icons.close),
|
||||
label: const Text('Clear'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// reusable helper for rendering a list given a subset of tasks
|
||||
@@ -803,25 +812,22 @@ class _TasksListScreenState extends ConsumerState<TasksListScreen>
|
||||
var showTitleGemini = false;
|
||||
Timer? titleTypingTimer;
|
||||
|
||||
await m3ShowDialog<void>(
|
||||
context: context,
|
||||
builder: (dialogContext) {
|
||||
bool saving = false;
|
||||
bool titleProcessing = false;
|
||||
bool descProcessing = false;
|
||||
bool titleDeepSeek = false;
|
||||
bool descDeepSeek = false;
|
||||
final officesAsync = ref.watch(officesProvider);
|
||||
return StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
return AlertDialog(
|
||||
shape: AppSurfaces.of(context).dialogShape,
|
||||
title: const Text('Create Task'),
|
||||
content: SizedBox(
|
||||
width: 360,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
final isMobile = MediaQuery.sizeOf(context).width < AppBreakpoints.tablet;
|
||||
|
||||
// Lifted so both mobile/desktop branches share one set of form state vars.
|
||||
var saving = false;
|
||||
var titleProcessing = false;
|
||||
var descProcessing = false;
|
||||
var titleDeepSeek = false;
|
||||
var descDeepSeek = false;
|
||||
|
||||
Column buildFormColumn(
|
||||
StateSetter setState,
|
||||
AsyncValue<List<Office>> officesAsync,
|
||||
) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -1038,77 +1044,126 @@ class _TasksListScreenState extends ConsumerState<TasksListScreen>
|
||||
error: (error, _) =>
|
||||
Text('Failed to load offices: $error'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> buildActions(BuildContext closeCtx, StateSetter setState) {
|
||||
return [
|
||||
TextButton(
|
||||
onPressed: saving ? null : () => Navigator.of(closeCtx).pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: saving
|
||||
? null
|
||||
: () async {
|
||||
final title = SubjectSuggestionEngine.normalizeDisplay(
|
||||
titleController.text.trim(),
|
||||
);
|
||||
final description = descriptionController.text.trim();
|
||||
final officeId = selectedOfficeId;
|
||||
if (title.isEmpty || officeId == null) return;
|
||||
setState(() => saving = true);
|
||||
try {
|
||||
await ref.read(tasksControllerProvider).createTask(
|
||||
title: title,
|
||||
description: description,
|
||||
officeId: officeId,
|
||||
requestType: selectedRequestType,
|
||||
requestTypeOther: requestTypeOther,
|
||||
requestCategory: selectedRequestCategory,
|
||||
);
|
||||
if (closeCtx.mounted) {
|
||||
Navigator.of(closeCtx).pop();
|
||||
showSuccessSnackBarGlobal(
|
||||
'Task "$title" has been created successfully.',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (!closeCtx.mounted) return;
|
||||
if (isOfflineSaveError(e)) {
|
||||
Navigator.of(closeCtx).pop();
|
||||
showSuccessSnackBarGlobal(
|
||||
'Task "$title" saved offline — will sync when connected.',
|
||||
);
|
||||
} else {
|
||||
showErrorSnackBarGlobal('Failed to create task: $e');
|
||||
}
|
||||
} finally {
|
||||
if (closeCtx.mounted) setState(() => saving = false);
|
||||
}
|
||||
},
|
||||
child: saving
|
||||
? const SizedBox(
|
||||
height: 18,
|
||||
width: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text('Create'),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
if (isMobile) {
|
||||
await m3ShowBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useSafeArea: true,
|
||||
builder: (ctx) {
|
||||
final officesAsync = ref.watch(officesProvider);
|
||||
return StatefulBuilder(
|
||||
builder: (context, setState) => Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 24,
|
||||
right: 24,
|
||||
top: 8,
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom + 24,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
'Create Task',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
buildFormColumn(setState, officesAsync),
|
||||
const SizedBox(height: 16),
|
||||
OverflowBar(
|
||||
alignment: MainAxisAlignment.end,
|
||||
children: buildActions(ctx, setState),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: saving
|
||||
? null
|
||||
: () => Navigator.of(dialogContext).pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: saving
|
||||
? null
|
||||
: () async {
|
||||
final title =
|
||||
SubjectSuggestionEngine.normalizeDisplay(
|
||||
titleController.text.trim(),
|
||||
);
|
||||
final description = descriptionController.text.trim();
|
||||
final officeId = selectedOfficeId;
|
||||
if (title.isEmpty || officeId == null) {
|
||||
return;
|
||||
}
|
||||
setState(() => saving = true);
|
||||
try {
|
||||
await ref
|
||||
.read(tasksControllerProvider)
|
||||
.createTask(
|
||||
title: title,
|
||||
description: description,
|
||||
officeId: officeId,
|
||||
requestType: selectedRequestType,
|
||||
requestTypeOther: requestTypeOther,
|
||||
requestCategory: selectedRequestCategory,
|
||||
);
|
||||
if (context.mounted) {
|
||||
Navigator.of(dialogContext).pop();
|
||||
showSuccessSnackBarGlobal(
|
||||
'Task "$title" has been created successfully.',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (!dialogContext.mounted) return;
|
||||
if (isOfflineSaveError(e)) {
|
||||
Navigator.of(dialogContext).pop();
|
||||
showSuccessSnackBarGlobal(
|
||||
'Task "$title" saved offline — will sync when connected.',
|
||||
);
|
||||
} else {
|
||||
showErrorSnackBarGlobal('Failed to create task: $e');
|
||||
}
|
||||
} finally {
|
||||
if (dialogContext.mounted) {
|
||||
setState(() => saving = false);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: saving
|
||||
? const SizedBox(
|
||||
height: 18,
|
||||
width: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text('Create'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
await m3ShowDialog<void>(
|
||||
context: context,
|
||||
builder: (dialogContext) {
|
||||
final officesAsync = ref.watch(officesProvider);
|
||||
return StatefulBuilder(
|
||||
builder: (context, setState) => AlertDialog(
|
||||
scrollable: true,
|
||||
shape: AppSurfaces.of(context).dialogShape,
|
||||
title: const Text('Create Task'),
|
||||
content: SizedBox(
|
||||
width: 600,
|
||||
child: buildFormColumn(setState, officesAsync),
|
||||
),
|
||||
actions: buildActions(dialogContext, setState),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
bool _hasTaskMention(
|
||||
|
||||
Reference in New Issue
Block a user