Add collapsible details

This commit is contained in:
Marc Rejohn Castillano 2026-02-21 15:19:15 +08:00
parent f8b8723d26
commit d2f1bcf9b3

View File

@ -232,7 +232,12 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
Text(description),
],
const SizedBox(height: 16),
// Tabbed details: Assignees / Type & Category / Signatories
// Collapsible tabbed details: Assignees / Type & Category / Signatories
ExpansionTile(
title: const Text('Details'),
initiallyExpanded: isWide,
childrenPadding: const EdgeInsets.symmetric(horizontal: 0),
children: [
DefaultTabController(
length: 3,
child: Column(
@ -257,7 +262,8 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
child: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
TaskAssignmentSection(
taskId: task.id,
@ -278,7 +284,8 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
child: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
if (!canUpdateStatus) ...[
_MetaBadge(
@ -313,7 +320,8 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
width: 16,
height: 16,
child: Stack(
alignment: Alignment.center,
alignment:
Alignment.center,
children: const [
Icon(
Icons.save,
@ -363,14 +371,17 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
);
} catch (_) {
} finally {
setState(() => _typeSaving = false);
setState(
() => _typeSaving = false,
);
if (_typeSaved) {
Future.delayed(
const Duration(seconds: 2),
() {
if (mounted) {
setState(
() => _typeSaved = false,
() =>
_typeSaved = false,
);
}
},
@ -402,7 +413,8 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
width: 16,
height: 16,
child: Stack(
alignment: Alignment.center,
alignment:
Alignment.center,
children: const [
Icon(
Icons.save,
@ -415,7 +427,8 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
child: Icon(
Icons.check,
size: 10,
color: Colors.white,
color:
Colors.white,
),
),
],
@ -430,27 +443,33 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
});
try {
await ref
.read(tasksControllerProvider)
.read(
tasksControllerProvider,
)
.updateTask(
taskId: task.id,
requestTypeOther: text.isEmpty
requestTypeOther:
text.isEmpty
? null
: text,
);
setState(
() =>
_typeSaved = text.isNotEmpty,
() => _typeSaved =
text.isNotEmpty,
);
} catch (_) {
} finally {
setState(() => _typeSaving = false);
setState(
() => _typeSaving = false,
);
if (_typeSaved) {
Future.delayed(
const Duration(seconds: 2),
() {
if (mounted) {
setState(
() => _typeSaved = false,
() => _typeSaved =
false,
);
}
},
@ -483,7 +502,8 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
width: 16,
height: 16,
child: Stack(
alignment: Alignment.center,
alignment:
Alignment.center,
children: const [
Icon(
Icons.save,
@ -509,7 +529,8 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
value: null,
child: Text('None'),
),
for (final c in requestCategoryOptions)
for (final c
in requestCategoryOptions)
DropdownMenuItem(
value: c,
child: Text(c),
@ -542,8 +563,8 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
() {
if (mounted) {
setState(
() =>
_categorySaved = false,
() => _categorySaved =
false,
);
}
},
@ -564,7 +585,8 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
child: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
'Requested by',
@ -595,7 +617,8 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
width: 16,
height: 16,
child: Stack(
alignment: Alignment.center,
alignment:
Alignment.center,
children: const [
Icon(
Icons.save,
@ -628,10 +651,13 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
});
try {
await ref
.read(tasksControllerProvider)
.read(
tasksControllerProvider,
)
.updateTask(
taskId: task.id,
requestedBy: name.isEmpty
requestedBy:
name.isEmpty
? null
: name,
);
@ -642,7 +668,9 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
supabaseClientProvider,
)
.from('clients')
.upsert({'name': name});
.upsert({
'name': name,
});
} catch (_) {}
}
setState(() {
@ -656,11 +684,14 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
});
if (_requestedSaved) {
Future.delayed(
const Duration(seconds: 2),
const Duration(
seconds: 2,
),
() {
if (mounted) {
setState(
() => _requestedSaved =
() =>
_requestedSaved =
false,
);
}
@ -699,7 +730,8 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
final clientNames =
(clientRows as List<dynamic>?)
?.map(
(r) => r['name'] as String,
(r) =>
r['name'] as String,
)
.whereType<String>()
.toList() ??
@ -715,28 +747,37 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
},
itemBuilder: (context, suggestion) =>
ListTile(title: Text(suggestion)),
onSuggestionSelected: (suggestion) async {
onSuggestionSelected:
(suggestion) async {
_requestedDebounce?.cancel();
_requestedController.text = suggestion;
_requestedController.text =
suggestion;
setState(() {
_requestedSaving = true;
_requestedSaved = false;
});
try {
await ref
.read(tasksControllerProvider)
.read(
tasksControllerProvider,
)
.updateTask(
taskId: task.id,
requestedBy: suggestion.isEmpty
requestedBy:
suggestion.isEmpty
? null
: suggestion,
);
if (suggestion.isNotEmpty) {
try {
await ref
.read(supabaseClientProvider)
.read(
supabaseClientProvider,
)
.from('clients')
.upsert({'name': suggestion});
.upsert({
'name': suggestion,
});
} catch (_) {}
}
setState(
@ -746,7 +787,8 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
} catch (_) {
} finally {
setState(
() => _requestedSaving = false,
() =>
_requestedSaving = false,
);
if (_requestedSaved) {
Future.delayed(
@ -754,7 +796,9 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
() {
if (mounted) {
setState(
() => _requestedSaved = false,
() =>
_requestedSaved =
false,
);
}
},
@ -794,7 +838,8 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
width: 16,
height: 16,
child: Stack(
alignment: Alignment.center,
alignment:
Alignment.center,
children: const [
Icon(
Icons.save,
@ -827,7 +872,9 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
});
try {
await ref
.read(tasksControllerProvider)
.read(
tasksControllerProvider,
)
.updateTask(
taskId: task.id,
notedBy: name.isEmpty
@ -841,11 +888,14 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
supabaseClientProvider,
)
.from('clients')
.upsert({'name': name});
.upsert({
'name': name,
});
} catch (_) {}
}
setState(() {
_notedSaved = name.isNotEmpty;
_notedSaved =
name.isNotEmpty;
});
} catch (_) {
// ignore
@ -855,12 +905,14 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
});
if (_notedSaved) {
Future.delayed(
const Duration(seconds: 2),
const Duration(
seconds: 2,
),
() {
if (mounted) {
setState(
() =>
_notedSaved = false,
() => _notedSaved =
false,
);
}
},
@ -898,7 +950,8 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
final clientNames =
(clientRows as List<dynamic>?)
?.map(
(r) => r['name'] as String,
(r) =>
r['name'] as String,
)
.whereType<String>()
.toList() ??
@ -914,28 +967,37 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
},
itemBuilder: (context, suggestion) =>
ListTile(title: Text(suggestion)),
onSuggestionSelected: (suggestion) async {
onSuggestionSelected:
(suggestion) async {
_notedDebounce?.cancel();
_notedController.text = suggestion;
_notedController.text =
suggestion;
setState(() {
_notedSaving = true;
_notedSaved = false;
});
try {
await ref
.read(tasksControllerProvider)
.read(
tasksControllerProvider,
)
.updateTask(
taskId: task.id,
notedBy: suggestion.isEmpty
notedBy:
suggestion.isEmpty
? null
: suggestion,
);
if (suggestion.isNotEmpty) {
try {
await ref
.read(supabaseClientProvider)
.read(
supabaseClientProvider,
)
.from('clients')
.upsert({'name': suggestion});
.upsert({
'name': suggestion,
});
} catch (_) {}
}
setState(
@ -944,14 +1006,17 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
);
} catch (_) {
} finally {
setState(() => _notedSaving = false);
setState(
() => _notedSaving = false,
);
if (_notedSaved) {
Future.delayed(
const Duration(seconds: 2),
() {
if (mounted) {
setState(
() => _notedSaved = false,
() => _notedSaved =
false,
);
}
},
@ -991,7 +1056,8 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
width: 16,
height: 16,
child: Stack(
alignment: Alignment.center,
alignment:
Alignment.center,
children: const [
Icon(
Icons.save,
@ -1024,7 +1090,9 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
});
try {
await ref
.read(tasksControllerProvider)
.read(
tasksControllerProvider,
)
.updateTask(
taskId: task.id,
receivedBy: name.isEmpty
@ -1038,7 +1106,9 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
supabaseClientProvider,
)
.from('clients')
.upsert({'name': name});
.upsert({
'name': name,
});
} catch (_) {}
}
setState(() {
@ -1053,11 +1123,14 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
});
if (_receivedSaved) {
Future.delayed(
const Duration(seconds: 2),
const Duration(
seconds: 2,
),
() {
if (mounted) {
setState(
() => _receivedSaved =
() =>
_receivedSaved =
false,
);
}
@ -1096,7 +1169,8 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
final clientNames =
(clientRows as List<dynamic>?)
?.map(
(r) => r['name'] as String,
(r) =>
r['name'] as String,
)
.whereType<String>()
.toList() ??
@ -1112,28 +1186,37 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
},
itemBuilder: (context, suggestion) =>
ListTile(title: Text(suggestion)),
onSuggestionSelected: (suggestion) async {
onSuggestionSelected:
(suggestion) async {
_receivedDebounce?.cancel();
_receivedController.text = suggestion;
_receivedController.text =
suggestion;
setState(() {
_receivedSaving = true;
_receivedSaved = false;
});
try {
await ref
.read(tasksControllerProvider)
.read(
tasksControllerProvider,
)
.updateTask(
taskId: task.id,
receivedBy: suggestion.isEmpty
receivedBy:
suggestion.isEmpty
? null
: suggestion,
);
if (suggestion.isNotEmpty) {
try {
await ref
.read(supabaseClientProvider)
.read(
supabaseClientProvider,
)
.from('clients')
.upsert({'name': suggestion});
.upsert({
'name': suggestion,
});
} catch (_) {}
}
setState(
@ -1142,14 +1225,17 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
);
} catch (_) {
} finally {
setState(() => _receivedSaving = false);
setState(
() => _receivedSaving = false,
);
if (_receivedSaved) {
Future.delayed(
const Duration(seconds: 2),
() {
if (mounted) {
setState(
() => _receivedSaved = false,
() => _receivedSaved =
false,
);
}
},
@ -1169,6 +1255,8 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
),
),
],
),
],
);
final detailsCard = Card(