Show saving indicator
This commit is contained in:
parent
8d31a629ac
commit
863f3151b3
|
|
@ -53,6 +53,18 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
|||
Timer? _requestedDebounce;
|
||||
Timer? _notedDebounce;
|
||||
Timer? _receivedDebounce;
|
||||
// Seeding/state tracking for signatory fields
|
||||
String? _seededTaskId;
|
||||
bool _requestedSaving = false;
|
||||
bool _requestedSaved = false;
|
||||
bool _notedSaving = false;
|
||||
bool _notedSaved = false;
|
||||
bool _receivedSaving = false;
|
||||
bool _receivedSaved = false;
|
||||
bool _typeSaving = false;
|
||||
bool _typeSaved = false;
|
||||
bool _categorySaving = false;
|
||||
bool _categorySaved = false;
|
||||
static const List<String> _statusOptions = [
|
||||
'queued',
|
||||
'in_progress',
|
||||
|
|
@ -100,7 +112,6 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
|||
child: Center(child: Text('Task not found.')),
|
||||
);
|
||||
}
|
||||
|
||||
final ticketId = task.ticketId;
|
||||
final typingChannelId = task.id;
|
||||
final ticket = ticketId == null
|
||||
|
|
@ -139,6 +150,17 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
|||
builder: (context, constraints) {
|
||||
final isWide = constraints.maxWidth >= AppBreakpoints.desktop;
|
||||
|
||||
// Seed controllers once per task to reflect persisted values
|
||||
if (_seededTaskId != task.id) {
|
||||
_seededTaskId = task.id;
|
||||
_requestedController.text = task.requestedBy ?? '';
|
||||
_notedController.text = task.notedBy ?? '';
|
||||
_receivedController.text = task.receivedBy ?? '';
|
||||
_requestedSaved = _requestedController.text.isNotEmpty;
|
||||
_notedSaved = _notedController.text.isNotEmpty;
|
||||
_receivedSaved = _receivedController.text.isNotEmpty;
|
||||
}
|
||||
|
||||
final detailsContent = Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
|
@ -158,7 +180,7 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
|||
child: Text(
|
||||
_createdByLabel(profilesAsync, task, ticket),
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
style: Theme.of(context).textTheme.labelMedium,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
|
@ -240,6 +262,24 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
|||
const SizedBox(height: 6),
|
||||
DropdownButtonFormField<String?>(
|
||||
initialValue: task.requestType,
|
||||
decoration: InputDecoration(
|
||||
suffixIcon: _typeSaving
|
||||
? const SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
child:
|
||||
CircularProgressIndicator(
|
||||
strokeWidth: 1.0,
|
||||
),
|
||||
)
|
||||
: _typeSaved
|
||||
? const Icon(
|
||||
Icons.check,
|
||||
color: Colors.green,
|
||||
size: 14,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
items: [
|
||||
const DropdownMenuItem(
|
||||
value: null,
|
||||
|
|
@ -251,28 +291,98 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
|||
child: Text(t),
|
||||
),
|
||||
],
|
||||
onChanged: (v) => ref
|
||||
onChanged: (v) async {
|
||||
setState(() {
|
||||
_typeSaving = true;
|
||||
_typeSaved = false;
|
||||
});
|
||||
try {
|
||||
await ref
|
||||
.read(tasksControllerProvider)
|
||||
.updateTask(
|
||||
taskId: task.id,
|
||||
requestType: v,
|
||||
),
|
||||
);
|
||||
setState(
|
||||
() => _typeSaved =
|
||||
v != null && v.isNotEmpty,
|
||||
);
|
||||
} catch (_) {
|
||||
} finally {
|
||||
setState(() => _typeSaving = false);
|
||||
if (_typeSaved) {
|
||||
Future.delayed(
|
||||
const Duration(seconds: 2),
|
||||
() {
|
||||
if (mounted) {
|
||||
setState(
|
||||
() => _typeSaved = false,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
if (task.requestType == 'Other') ...[
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
initialValue: task.requestTypeOther,
|
||||
decoration: const InputDecoration(
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Details',
|
||||
suffixIcon: _typeSaving
|
||||
? const SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
child:
|
||||
CircularProgressIndicator(
|
||||
strokeWidth: 1.0,
|
||||
),
|
||||
onChanged: (text) => ref
|
||||
)
|
||||
: _typeSaved
|
||||
? const Icon(
|
||||
Icons.check,
|
||||
color: Colors.green,
|
||||
size: 14,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
onChanged: (text) async {
|
||||
setState(() {
|
||||
_typeSaving = true;
|
||||
_typeSaved = false;
|
||||
});
|
||||
try {
|
||||
await ref
|
||||
.read(tasksControllerProvider)
|
||||
.updateTask(
|
||||
taskId: task.id,
|
||||
requestTypeOther: text.isEmpty
|
||||
? null
|
||||
: text,
|
||||
),
|
||||
);
|
||||
setState(
|
||||
() =>
|
||||
_typeSaved = text.isNotEmpty,
|
||||
);
|
||||
} catch (_) {
|
||||
} finally {
|
||||
setState(() => _typeSaving = false);
|
||||
if (_typeSaved) {
|
||||
Future.delayed(
|
||||
const Duration(seconds: 2),
|
||||
() {
|
||||
if (mounted) {
|
||||
setState(
|
||||
() => _typeSaved = false,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 8),
|
||||
|
|
@ -280,6 +390,24 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
|||
const SizedBox(height: 6),
|
||||
DropdownButtonFormField<String?>(
|
||||
initialValue: task.requestCategory,
|
||||
decoration: InputDecoration(
|
||||
suffixIcon: _categorySaving
|
||||
? const SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
child:
|
||||
CircularProgressIndicator(
|
||||
strokeWidth: 1.0,
|
||||
),
|
||||
)
|
||||
: _categorySaved
|
||||
? const Icon(
|
||||
Icons.check,
|
||||
color: Colors.green,
|
||||
size: 14,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
items: [
|
||||
const DropdownMenuItem(
|
||||
value: null,
|
||||
|
|
@ -291,12 +419,42 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
|||
child: Text(c),
|
||||
),
|
||||
],
|
||||
onChanged: (v) => ref
|
||||
onChanged: (v) async {
|
||||
setState(() {
|
||||
_categorySaving = true;
|
||||
_categorySaved = false;
|
||||
});
|
||||
try {
|
||||
await ref
|
||||
.read(tasksControllerProvider)
|
||||
.updateTask(
|
||||
taskId: task.id,
|
||||
requestCategory: v,
|
||||
),
|
||||
);
|
||||
setState(
|
||||
() => _categorySaved =
|
||||
v != null && v.isNotEmpty,
|
||||
);
|
||||
} catch (_) {
|
||||
} finally {
|
||||
setState(
|
||||
() => _categorySaving = false,
|
||||
);
|
||||
if (_categorySaved) {
|
||||
Future.delayed(
|
||||
const Duration(seconds: 2),
|
||||
() {
|
||||
if (mounted) {
|
||||
setState(
|
||||
() =>
|
||||
_categorySaved = false,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
|
|
@ -320,11 +478,26 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
|||
),
|
||||
const SizedBox(height: 6),
|
||||
TypeAheadFormField<String>(
|
||||
textFieldConfiguration:
|
||||
TextFieldConfiguration(
|
||||
textFieldConfiguration: TextFieldConfiguration(
|
||||
controller: _requestedController,
|
||||
decoration: const InputDecoration(
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Requester name or id',
|
||||
suffixIcon: _requestedSaving
|
||||
? const SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
child:
|
||||
CircularProgressIndicator(
|
||||
strokeWidth: 1.0,
|
||||
),
|
||||
)
|
||||
: _requestedSaved
|
||||
? const Icon(
|
||||
Icons.check,
|
||||
color: Colors.green,
|
||||
size: 14,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
onChanged: (v) {
|
||||
_requestedDebounce?.cancel();
|
||||
|
|
@ -332,10 +505,13 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
|||
const Duration(milliseconds: 700),
|
||||
() async {
|
||||
final name = v.trim();
|
||||
setState(() {
|
||||
_requestedSaving = true;
|
||||
_requestedSaved = false;
|
||||
});
|
||||
try {
|
||||
await ref
|
||||
.read(
|
||||
tasksControllerProvider,
|
||||
)
|
||||
.read(tasksControllerProvider)
|
||||
.updateTask(
|
||||
taskId: task.id,
|
||||
requestedBy: name.isEmpty
|
||||
|
|
@ -352,6 +528,29 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
|||
.upsert({'name': name});
|
||||
} catch (_) {}
|
||||
}
|
||||
setState(() {
|
||||
_requestedSaved =
|
||||
name.isNotEmpty;
|
||||
});
|
||||
} catch (_) {
|
||||
} finally {
|
||||
setState(() {
|
||||
_requestedSaving = false;
|
||||
});
|
||||
if (_requestedSaved) {
|
||||
Future.delayed(
|
||||
const Duration(seconds: 2),
|
||||
() {
|
||||
if (mounted) {
|
||||
setState(
|
||||
() => _requestedSaved =
|
||||
false,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
|
|
@ -402,6 +601,11 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
|||
onSuggestionSelected: (suggestion) async {
|
||||
_requestedDebounce?.cancel();
|
||||
_requestedController.text = suggestion;
|
||||
setState(() {
|
||||
_requestedSaving = true;
|
||||
_requestedSaved = false;
|
||||
});
|
||||
try {
|
||||
await ref
|
||||
.read(tasksControllerProvider)
|
||||
.updateTask(
|
||||
|
|
@ -410,14 +614,36 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
|||
? null
|
||||
: suggestion,
|
||||
);
|
||||
try {
|
||||
if (suggestion.isNotEmpty) {
|
||||
try {
|
||||
await ref
|
||||
.read(supabaseClientProvider)
|
||||
.from('clients')
|
||||
.upsert({'name': suggestion});
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
setState(
|
||||
() => _requestedSaved =
|
||||
suggestion.isNotEmpty,
|
||||
);
|
||||
} catch (_) {
|
||||
} finally {
|
||||
setState(
|
||||
() => _requestedSaving = false,
|
||||
);
|
||||
if (_requestedSaved) {
|
||||
Future.delayed(
|
||||
const Duration(seconds: 2),
|
||||
() {
|
||||
if (mounted) {
|
||||
setState(
|
||||
() => _requestedSaved = false,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
|
|
@ -430,11 +656,26 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
|||
),
|
||||
const SizedBox(height: 6),
|
||||
TypeAheadFormField<String>(
|
||||
textFieldConfiguration:
|
||||
TextFieldConfiguration(
|
||||
textFieldConfiguration: TextFieldConfiguration(
|
||||
controller: _notedController,
|
||||
decoration: const InputDecoration(
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Supervisor/Senior',
|
||||
suffixIcon: _notedSaving
|
||||
? const SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
child:
|
||||
CircularProgressIndicator(
|
||||
strokeWidth: 1.0,
|
||||
),
|
||||
)
|
||||
: _notedSaved
|
||||
? const Icon(
|
||||
Icons.check,
|
||||
color: Colors.green,
|
||||
size: 14,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
onChanged: (v) {
|
||||
_notedDebounce?.cancel();
|
||||
|
|
@ -442,10 +683,13 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
|||
const Duration(milliseconds: 700),
|
||||
() async {
|
||||
final name = v.trim();
|
||||
setState(() {
|
||||
_notedSaving = true;
|
||||
_notedSaved = false;
|
||||
});
|
||||
try {
|
||||
await ref
|
||||
.read(
|
||||
tasksControllerProvider,
|
||||
)
|
||||
.read(tasksControllerProvider)
|
||||
.updateTask(
|
||||
taskId: task.id,
|
||||
notedBy: name.isEmpty
|
||||
|
|
@ -462,6 +706,29 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
|||
.upsert({'name': name});
|
||||
} catch (_) {}
|
||||
}
|
||||
setState(() {
|
||||
_notedSaved = name.isNotEmpty;
|
||||
});
|
||||
} catch (_) {
|
||||
// ignore
|
||||
} finally {
|
||||
setState(() {
|
||||
_notedSaving = false;
|
||||
});
|
||||
if (_notedSaved) {
|
||||
Future.delayed(
|
||||
const Duration(seconds: 2),
|
||||
() {
|
||||
if (mounted) {
|
||||
setState(
|
||||
() =>
|
||||
_notedSaved = false,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
|
|
@ -512,6 +779,11 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
|||
onSuggestionSelected: (suggestion) async {
|
||||
_notedDebounce?.cancel();
|
||||
_notedController.text = suggestion;
|
||||
setState(() {
|
||||
_notedSaving = true;
|
||||
_notedSaved = false;
|
||||
});
|
||||
try {
|
||||
await ref
|
||||
.read(tasksControllerProvider)
|
||||
.updateTask(
|
||||
|
|
@ -520,14 +792,34 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
|||
? null
|
||||
: suggestion,
|
||||
);
|
||||
try {
|
||||
if (suggestion.isNotEmpty) {
|
||||
try {
|
||||
await ref
|
||||
.read(supabaseClientProvider)
|
||||
.from('clients')
|
||||
.upsert({'name': suggestion});
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
setState(
|
||||
() => _notedSaved =
|
||||
suggestion.isNotEmpty,
|
||||
);
|
||||
} catch (_) {
|
||||
} finally {
|
||||
setState(() => _notedSaving = false);
|
||||
if (_notedSaved) {
|
||||
Future.delayed(
|
||||
const Duration(seconds: 2),
|
||||
() {
|
||||
if (mounted) {
|
||||
setState(
|
||||
() => _notedSaved = false,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
|
|
@ -540,11 +832,26 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
|||
),
|
||||
const SizedBox(height: 6),
|
||||
TypeAheadFormField<String>(
|
||||
textFieldConfiguration:
|
||||
TextFieldConfiguration(
|
||||
textFieldConfiguration: TextFieldConfiguration(
|
||||
controller: _receivedController,
|
||||
decoration: const InputDecoration(
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Receiver name or id',
|
||||
suffixIcon: _receivedSaving
|
||||
? const SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
child:
|
||||
CircularProgressIndicator(
|
||||
strokeWidth: 1.0,
|
||||
),
|
||||
)
|
||||
: _receivedSaved
|
||||
? const Icon(
|
||||
Icons.check,
|
||||
color: Colors.green,
|
||||
size: 14,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
onChanged: (v) {
|
||||
_receivedDebounce?.cancel();
|
||||
|
|
@ -553,9 +860,7 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
|||
() async {
|
||||
final name = v.trim();
|
||||
await ref
|
||||
.read(
|
||||
tasksControllerProvider,
|
||||
)
|
||||
.read(tasksControllerProvider)
|
||||
.updateTask(
|
||||
taskId: task.id,
|
||||
receivedBy: name.isEmpty
|
||||
|
|
@ -622,6 +927,11 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
|||
onSuggestionSelected: (suggestion) async {
|
||||
_receivedDebounce?.cancel();
|
||||
_receivedController.text = suggestion;
|
||||
setState(() {
|
||||
_receivedSaving = true;
|
||||
_receivedSaved = false;
|
||||
});
|
||||
try {
|
||||
await ref
|
||||
.read(tasksControllerProvider)
|
||||
.updateTask(
|
||||
|
|
@ -630,14 +940,34 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen> {
|
|||
? null
|
||||
: suggestion,
|
||||
);
|
||||
try {
|
||||
if (suggestion.isNotEmpty) {
|
||||
try {
|
||||
await ref
|
||||
.read(supabaseClientProvider)
|
||||
.from('clients')
|
||||
.upsert({'name': suggestion});
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
setState(
|
||||
() => _receivedSaved =
|
||||
suggestion.isNotEmpty,
|
||||
);
|
||||
} catch (_) {
|
||||
} finally {
|
||||
setState(() => _receivedSaving = false);
|
||||
if (_receivedSaved) {
|
||||
Future.delayed(
|
||||
const Duration(seconds: 2),
|
||||
() {
|
||||
if (mounted) {
|
||||
setState(
|
||||
() => _receivedSaved = false,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user