diff --git a/lib/screens/tasks/task_detail_screen.dart b/lib/screens/tasks/task_detail_screen.dart index 82c07510..3342eb7b 100644 --- a/lib/screens/tasks/task_detail_screen.dart +++ b/lib/screens/tasks/task_detail_screen.dart @@ -232,635 +232,156 @@ class _TaskDetailScreenState extends ConsumerState Text(description), ], const SizedBox(height: 16), - // Tabbed details: Assignees / Type & Category / Signatories - DefaultTabController( - length: 3, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TabBar( - labelColor: Theme.of(context).colorScheme.onSurface, - indicatorColor: Theme.of(context).colorScheme.primary, - tabs: const [ - Tab(text: 'Assignees'), - Tab(text: 'Type & Category'), - Tab(text: 'Signatories'), - ], - ), - const SizedBox(height: 8), - SizedBox( - height: isWide ? 360 : 300, - child: TabBarView( - children: [ - // Assignees - SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TaskAssignmentSection( - taskId: task.id, - canAssign: showAssign, - ), - const SizedBox(height: 12), - Align( - alignment: Alignment.bottomRight, - child: _buildTatSection(task), - ), - ], - ), - ), - ), - - // Type & Category - SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (!canUpdateStatus) ...[ - _MetaBadge( - label: 'Type', - value: task.requestType ?? 'None', - ), - const SizedBox(height: 8), - _MetaBadge( - label: 'Category', - value: task.requestCategory ?? 'None', - ), - ] else ...[ - const Text('Type'), - const SizedBox(height: 6), - DropdownButtonFormField( - initialValue: task.requestType, - decoration: InputDecoration( - suffixIcon: _typeSaving - ? SizedBox( - width: 16, - height: 16, - child: ScaleTransition( - scale: _savePulse, - child: const Icon( - Icons.save, - size: 14, - ), - ), - ) - : _typeSaved - ? SizedBox( - width: 16, - height: 16, - child: Stack( - alignment: Alignment.center, - children: const [ - Icon( - Icons.save, - size: 14, - color: Colors.green, - ), - Positioned( - right: -2, - bottom: -2, - child: Icon( - Icons.check, - size: 10, - color: Colors.white, - ), - ), - ], - ), - ) - : null, + // 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( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TabBar( + labelColor: Theme.of(context).colorScheme.onSurface, + indicatorColor: Theme.of(context).colorScheme.primary, + tabs: const [ + Tab(text: 'Assignees'), + Tab(text: 'Type & Category'), + Tab(text: 'Signatories'), + ], + ), + const SizedBox(height: 8), + SizedBox( + height: isWide ? 360 : 300, + child: TabBarView( + children: [ + // Assignees + SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + TaskAssignmentSection( + taskId: task.id, + canAssign: showAssign, ), - items: [ - const DropdownMenuItem( - value: null, - child: Text('None'), - ), - for (final t in requestTypeOptions) - DropdownMenuItem( - value: t, - child: Text(t), - ), - ], - 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: InputDecoration( - hintText: 'Details', - suffixIcon: _typeSaving - ? SizedBox( - width: 16, - height: 16, - child: ScaleTransition( - scale: _savePulse, - child: const Icon( - Icons.save, - size: 14, - ), - ), - ) - : _typeSaved - ? SizedBox( - width: 16, - height: 16, - child: Stack( - alignment: Alignment.center, - children: const [ - Icon( - Icons.save, - size: 14, - color: Colors.green, - ), - Positioned( - right: -2, - bottom: -2, - child: Icon( - Icons.check, - size: 10, - color: Colors.white, - ), - ), - ], - ), - ) - : 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: 12), + Align( + alignment: Alignment.bottomRight, + child: _buildTatSection(task), ), ], - const SizedBox(height: 8), - const Text('Category'), - const SizedBox(height: 6), - DropdownButtonFormField( - initialValue: task.requestCategory, - decoration: InputDecoration( - suffixIcon: _categorySaving - ? SizedBox( - width: 16, - height: 16, - child: ScaleTransition( - scale: _savePulse, - child: const Icon( - Icons.save, - size: 14, - ), - ), - ) - : _categorySaved - ? SizedBox( - width: 16, - height: 16, - child: Stack( - alignment: Alignment.center, - children: const [ - Icon( - Icons.save, - size: 14, - color: Colors.green, - ), - Positioned( - right: -2, - bottom: -2, - child: Icon( - Icons.check, - size: 10, - color: Colors.white, - ), - ), - ], - ), - ) - : null, - ), - items: [ - const DropdownMenuItem( - value: null, - child: Text('None'), - ), - for (final c in requestCategoryOptions) - DropdownMenuItem( - value: c, - child: Text(c), - ), - ], - 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), - ], + ), + ), ), - ), - ), - // Signatories (editable) - SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Requested by', - style: Theme.of( - context, - ).textTheme.bodySmall, - ), - const SizedBox(height: 6), - TypeAheadFormField( - textFieldConfiguration: TextFieldConfiguration( - controller: _requestedController, - decoration: InputDecoration( - hintText: 'Requester name or id', - suffixIcon: _requestedSaving - ? SizedBox( - width: 16, - height: 16, - child: ScaleTransition( - scale: _savePulse, - child: const Icon( - Icons.save, - size: 14, - ), - ), - ) - : _requestedSaved - ? SizedBox( - width: 16, - height: 16, - child: Stack( - alignment: Alignment.center, - children: const [ - Icon( - Icons.save, - size: 14, - color: Colors.green, - ), - Positioned( - right: -2, - bottom: -2, - child: Icon( - Icons.check, - size: 10, - color: Colors.white, + // Type & Category + SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + if (!canUpdateStatus) ...[ + _MetaBadge( + label: 'Type', + value: task.requestType ?? 'None', + ), + const SizedBox(height: 8), + _MetaBadge( + label: 'Category', + value: task.requestCategory ?? 'None', + ), + ] else ...[ + const Text('Type'), + const SizedBox(height: 6), + DropdownButtonFormField( + initialValue: task.requestType, + decoration: InputDecoration( + suffixIcon: _typeSaving + ? SizedBox( + width: 16, + height: 16, + child: ScaleTransition( + scale: _savePulse, + child: const Icon( + Icons.save, + size: 14, ), ), - ], - ), - ) - : null, - ), - onChanged: (v) { - _requestedDebounce?.cancel(); - _requestedDebounce = Timer( - const Duration(milliseconds: 700), - () async { - final name = v.trim(); - setState(() { - _requestedSaving = true; - _requestedSaved = false; - }); - try { - await ref - .read(tasksControllerProvider) - .updateTask( - taskId: task.id, - requestedBy: name.isEmpty - ? null - : name, - ); - if (name.isNotEmpty) { - try { - await ref - .read( - supabaseClientProvider, - ) - .from('clients') - .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, - ); - } - }, - ); - } - } - }, - ); - }, - ), - suggestionsCallback: (pattern) async { - final profiles = - ref - .watch(profilesProvider) - .valueOrNull ?? - []; - final fromProfiles = profiles - .map( - (p) => p.fullName.isEmpty - ? p.id - : p.fullName, - ) - .where( - (n) => n.toLowerCase().contains( - pattern.toLowerCase(), + ) + : _typeSaved + ? SizedBox( + width: 16, + height: 16, + child: Stack( + alignment: + Alignment.center, + children: const [ + Icon( + Icons.save, + size: 14, + color: Colors.green, + ), + Positioned( + right: -2, + bottom: -2, + child: Icon( + Icons.check, + size: 10, + color: Colors.white, + ), + ), + ], + ), + ) + : null, + ), + items: [ + const DropdownMenuItem( + value: null, + child: Text('None'), ), - ) - .toList(); - try { - final clientRows = await ref - .read(supabaseClientProvider) - .from('clients') - .select('name') - .ilike('name', '%$pattern%'); - final clientNames = - (clientRows as List?) - ?.map( - (r) => r['name'] as String, - ) - .whereType() - .toList() ?? - []; - final merged = { - ...fromProfiles, - ...clientNames, - }.toList(); - return merged; - } catch (_) { - return fromProfiles; - } - }, - itemBuilder: (context, suggestion) => - ListTile(title: Text(suggestion)), - onSuggestionSelected: (suggestion) async { - _requestedDebounce?.cancel(); - _requestedController.text = suggestion; - setState(() { - _requestedSaving = true; - _requestedSaved = false; - }); - try { - await ref - .read(tasksControllerProvider) - .updateTask( - taskId: task.id, - requestedBy: suggestion.isEmpty - ? null - : suggestion, - ); - 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, - ); - } - }, - ); - } - } - }, - ), - - const SizedBox(height: 12), - Text( - 'Noted by (Supervisor/Senior)', - style: Theme.of( - context, - ).textTheme.bodySmall, - ), - const SizedBox(height: 6), - TypeAheadFormField( - textFieldConfiguration: TextFieldConfiguration( - controller: _notedController, - decoration: InputDecoration( - hintText: 'Supervisor/Senior', - suffixIcon: _notedSaving - ? SizedBox( - width: 16, - height: 16, - child: ScaleTransition( - scale: _savePulse, - child: const Icon( - Icons.save, - size: 14, - ), - ), - ) - : _notedSaved - ? SizedBox( - width: 16, - height: 16, - child: Stack( - alignment: Alignment.center, - children: const [ - Icon( - Icons.save, - size: 14, - color: Colors.green, - ), - Positioned( - right: -2, - bottom: -2, - child: Icon( - Icons.check, - size: 10, - color: Colors.white, - ), - ), - ], - ), - ) - : null, - ), - onChanged: (v) { - _notedDebounce?.cancel(); - _notedDebounce = Timer( - const Duration(milliseconds: 700), - () async { - final name = v.trim(); + for (final t in requestTypeOptions) + DropdownMenuItem( + value: t, + child: Text(t), + ), + ], + onChanged: (v) async { setState(() { - _notedSaving = true; - _notedSaved = false; + _typeSaving = true; + _typeSaved = false; }); try { await ref .read(tasksControllerProvider) .updateTask( taskId: task.id, - notedBy: name.isEmpty - ? null - : name, + requestType: v, ); - if (name.isNotEmpty) { - try { - await ref - .read( - supabaseClientProvider, - ) - .from('clients') - .upsert({'name': name}); - } catch (_) {} - } - setState(() { - _notedSaved = name.isNotEmpty; - }); + setState( + () => _typeSaved = + v != null && v.isNotEmpty, + ); } catch (_) { - // ignore } finally { - setState(() { - _notedSaving = false; - }); - if (_notedSaved) { + setState( + () => _typeSaving = false, + ); + if (_typeSaved) { Future.delayed( const Duration(seconds: 2), () { if (mounted) { setState( () => - _notedSaved = false, + _typeSaved = false, ); } }, @@ -868,196 +389,181 @@ class _TaskDetailScreenState extends ConsumerState } } }, - ); - }, - ), - suggestionsCallback: (pattern) async { - final profiles = - ref - .watch(profilesProvider) - .valueOrNull ?? - []; - final fromProfiles = profiles - .map( - (p) => p.fullName.isEmpty - ? p.id - : p.fullName, - ) - .where( - (n) => n.toLowerCase().contains( - pattern.toLowerCase(), + ), + if (task.requestType == 'Other') ...[ + const SizedBox(height: 8), + TextFormField( + initialValue: task.requestTypeOther, + decoration: InputDecoration( + hintText: 'Details', + suffixIcon: _typeSaving + ? SizedBox( + width: 16, + height: 16, + child: ScaleTransition( + scale: _savePulse, + child: const Icon( + Icons.save, + size: 14, + ), + ), + ) + : _typeSaved + ? SizedBox( + width: 16, + height: 16, + child: Stack( + alignment: + Alignment.center, + children: const [ + Icon( + Icons.save, + size: 14, + color: Colors.green, + ), + Positioned( + right: -2, + bottom: -2, + child: Icon( + Icons.check, + size: 10, + color: + Colors.white, + ), + ), + ], + ), + ) + : null, ), - ) - .toList(); - try { - final clientRows = await ref - .read(supabaseClientProvider) - .from('clients') - .select('name') - .ilike('name', '%$pattern%'); - final clientNames = - (clientRows as List?) - ?.map( - (r) => r['name'] as String, - ) - .whereType() - .toList() ?? - []; - final merged = { - ...fromProfiles, - ...clientNames, - }.toList(); - return merged; - } catch (_) { - return fromProfiles; - } - }, - itemBuilder: (context, suggestion) => - ListTile(title: Text(suggestion)), - onSuggestionSelected: (suggestion) async { - _notedDebounce?.cancel(); - _notedController.text = suggestion; - setState(() { - _notedSaving = true; - _notedSaved = false; - }); - try { - await ref - .read(tasksControllerProvider) - .updateTask( - taskId: task.id, - notedBy: suggestion.isEmpty - ? null - : suggestion, - ); - 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) { + onChanged: (text) async { + setState(() { + _typeSaving = true; + _typeSaved = false; + }); + try { + await ref + .read( + tasksControllerProvider, + ) + .updateTask( + taskId: task.id, + requestTypeOther: + text.isEmpty + ? null + : text, + ); setState( - () => _notedSaved = false, + () => _typeSaved = + text.isNotEmpty, ); + } catch (_) { + } finally { + setState( + () => _typeSaving = false, + ); + if (_typeSaved) { + Future.delayed( + const Duration(seconds: 2), + () { + if (mounted) { + setState( + () => _typeSaved = + false, + ); + } + }, + ); + } } }, - ); - } - } - }, - ), - - const SizedBox(height: 12), - Text( - 'Received by', - style: Theme.of( - context, - ).textTheme.bodySmall, - ), - const SizedBox(height: 6), - TypeAheadFormField( - textFieldConfiguration: TextFieldConfiguration( - controller: _receivedController, - decoration: InputDecoration( - hintText: 'Receiver name or id', - suffixIcon: _receivedSaving - ? SizedBox( - width: 16, - height: 16, - child: ScaleTransition( - scale: _savePulse, - child: const Icon( - Icons.save, - size: 14, - ), - ), - ) - : _receivedSaved - ? SizedBox( - width: 16, - height: 16, - child: Stack( - alignment: Alignment.center, - children: const [ - Icon( - Icons.save, - size: 14, - color: Colors.green, - ), - Positioned( - right: -2, - bottom: -2, - child: Icon( - Icons.check, - size: 10, - color: Colors.white, + ), + ], + const SizedBox(height: 8), + const Text('Category'), + const SizedBox(height: 6), + DropdownButtonFormField( + initialValue: task.requestCategory, + decoration: InputDecoration( + suffixIcon: _categorySaving + ? SizedBox( + width: 16, + height: 16, + child: ScaleTransition( + scale: _savePulse, + child: const Icon( + Icons.save, + size: 14, ), ), - ], - ), - ) - : null, - ), - onChanged: (v) { - _receivedDebounce?.cancel(); - _receivedDebounce = Timer( - const Duration(milliseconds: 700), - () async { - final name = v.trim(); + ) + : _categorySaved + ? SizedBox( + width: 16, + height: 16, + child: Stack( + alignment: + Alignment.center, + children: const [ + Icon( + Icons.save, + size: 14, + color: Colors.green, + ), + Positioned( + right: -2, + bottom: -2, + child: Icon( + Icons.check, + size: 10, + color: Colors.white, + ), + ), + ], + ), + ) + : null, + ), + items: [ + const DropdownMenuItem( + value: null, + child: Text('None'), + ), + for (final c + in requestCategoryOptions) + DropdownMenuItem( + value: c, + child: Text(c), + ), + ], + onChanged: (v) async { setState(() { - _receivedSaving = true; - _receivedSaved = false; + _categorySaving = true; + _categorySaved = false; }); try { await ref .read(tasksControllerProvider) .updateTask( taskId: task.id, - receivedBy: name.isEmpty - ? null - : name, + requestCategory: v, ); - if (name.isNotEmpty) { - try { - await ref - .read( - supabaseClientProvider, - ) - .from('clients') - .upsert({'name': name}); - } catch (_) {} - } - setState(() { - _receivedSaved = - name.isNotEmpty; - }); + setState( + () => _categorySaved = + v != null && v.isNotEmpty, + ); } catch (_) { - // ignore } finally { - setState(() { - _receivedSaving = false; - }); - if (_receivedSaved) { + setState( + () => _categorySaving = false, + ); + if (_categorySaved) { Future.delayed( const Duration(seconds: 2), () { if (mounted) { setState( - () => _receivedSaved = + () => _categorySaved = false, ); } @@ -1066,107 +572,689 @@ class _TaskDetailScreenState extends ConsumerState } } }, - ); - }, - ), - suggestionsCallback: (pattern) async { - final profiles = - ref - .watch(profilesProvider) - .valueOrNull ?? - []; - final fromProfiles = profiles - .map( - (p) => p.fullName.isEmpty - ? p.id - : p.fullName, - ) - .where( - (n) => n.toLowerCase().contains( - pattern.toLowerCase(), - ), - ) - .toList(); - try { - final clientRows = await ref - .read(supabaseClientProvider) - .from('clients') - .select('name') - .ilike('name', '%$pattern%'); - final clientNames = - (clientRows as List?) - ?.map( - (r) => r['name'] as String, - ) - .whereType() - .toList() ?? - []; - final merged = { - ...fromProfiles, - ...clientNames, - }.toList(); - return merged; - } catch (_) { - return fromProfiles; - } - }, - itemBuilder: (context, suggestion) => - ListTile(title: Text(suggestion)), - onSuggestionSelected: (suggestion) async { - _receivedDebounce?.cancel(); - _receivedController.text = suggestion; - setState(() { - _receivedSaving = true; - _receivedSaved = false; - }); - try { - await ref - .read(tasksControllerProvider) - .updateTask( - taskId: task.id, - receivedBy: suggestion.isEmpty - ? null - : suggestion, + ), + ], + const SizedBox(height: 12), + ], + ), + ), + ), + + // Signatories (editable) + SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + 'Requested by', + style: Theme.of( + context, + ).textTheme.bodySmall, + ), + const SizedBox(height: 6), + TypeAheadFormField( + textFieldConfiguration: TextFieldConfiguration( + controller: _requestedController, + decoration: InputDecoration( + hintText: 'Requester name or id', + suffixIcon: _requestedSaving + ? SizedBox( + width: 16, + height: 16, + child: ScaleTransition( + scale: _savePulse, + child: const Icon( + Icons.save, + size: 14, + ), + ), + ) + : _requestedSaved + ? SizedBox( + width: 16, + height: 16, + child: Stack( + alignment: + Alignment.center, + children: const [ + Icon( + Icons.save, + size: 14, + color: Colors.green, + ), + Positioned( + right: -2, + bottom: -2, + child: Icon( + Icons.check, + size: 10, + color: Colors.white, + ), + ), + ], + ), + ) + : null, + ), + onChanged: (v) { + _requestedDebounce?.cancel(); + _requestedDebounce = Timer( + const Duration(milliseconds: 700), + () async { + final name = v.trim(); + setState(() { + _requestedSaving = true; + _requestedSaved = false; + }); + try { + await ref + .read( + tasksControllerProvider, + ) + .updateTask( + taskId: task.id, + requestedBy: + name.isEmpty + ? null + : name, + ); + if (name.isNotEmpty) { + try { + await ref + .read( + supabaseClientProvider, + ) + .from('clients') + .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, + ); + } + }, + ); + } + } + }, ); - if (suggestion.isNotEmpty) { + }, + ), + suggestionsCallback: (pattern) async { + final profiles = + ref + .watch(profilesProvider) + .valueOrNull ?? + []; + final fromProfiles = profiles + .map( + (p) => p.fullName.isEmpty + ? p.id + : p.fullName, + ) + .where( + (n) => n.toLowerCase().contains( + pattern.toLowerCase(), + ), + ) + .toList(); try { - await ref + final clientRows = 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) { + .select('name') + .ilike('name', '%$pattern%'); + final clientNames = + (clientRows as List?) + ?.map( + (r) => + r['name'] as String, + ) + .whereType() + .toList() ?? + []; + final merged = { + ...fromProfiles, + ...clientNames, + }.toList(); + return merged; + } catch (_) { + return fromProfiles; + } + }, + itemBuilder: (context, suggestion) => + ListTile(title: Text(suggestion)), + onSuggestionSelected: + (suggestion) async { + _requestedDebounce?.cancel(); + _requestedController.text = + suggestion; + setState(() { + _requestedSaving = true; + _requestedSaved = false; + }); + try { + await ref + .read( + tasksControllerProvider, + ) + .updateTask( + taskId: task.id, + requestedBy: + suggestion.isEmpty + ? null + : suggestion, + ); + if (suggestion.isNotEmpty) { + try { + await ref + .read( + supabaseClientProvider, + ) + .from('clients') + .upsert({ + 'name': suggestion, + }); + } catch (_) {} + } setState( - () => _receivedSaved = false, + () => _requestedSaved = + suggestion.isNotEmpty, ); + } catch (_) { + } finally { + setState( + () => + _requestedSaving = false, + ); + if (_requestedSaved) { + Future.delayed( + const Duration(seconds: 2), + () { + if (mounted) { + setState( + () => + _requestedSaved = + false, + ); + } + }, + ); + } } }, - ); - } - } - }, + ), + + const SizedBox(height: 12), + Text( + 'Noted by (Supervisor/Senior)', + style: Theme.of( + context, + ).textTheme.bodySmall, + ), + const SizedBox(height: 6), + TypeAheadFormField( + textFieldConfiguration: TextFieldConfiguration( + controller: _notedController, + decoration: InputDecoration( + hintText: 'Supervisor/Senior', + suffixIcon: _notedSaving + ? SizedBox( + width: 16, + height: 16, + child: ScaleTransition( + scale: _savePulse, + child: const Icon( + Icons.save, + size: 14, + ), + ), + ) + : _notedSaved + ? SizedBox( + width: 16, + height: 16, + child: Stack( + alignment: + Alignment.center, + children: const [ + Icon( + Icons.save, + size: 14, + color: Colors.green, + ), + Positioned( + right: -2, + bottom: -2, + child: Icon( + Icons.check, + size: 10, + color: Colors.white, + ), + ), + ], + ), + ) + : null, + ), + onChanged: (v) { + _notedDebounce?.cancel(); + _notedDebounce = Timer( + const Duration(milliseconds: 700), + () async { + final name = v.trim(); + setState(() { + _notedSaving = true; + _notedSaved = false; + }); + try { + await ref + .read( + tasksControllerProvider, + ) + .updateTask( + taskId: task.id, + notedBy: name.isEmpty + ? null + : name, + ); + if (name.isNotEmpty) { + try { + await ref + .read( + supabaseClientProvider, + ) + .from('clients') + .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, + ); + } + }, + ); + } + } + }, + ); + }, + ), + suggestionsCallback: (pattern) async { + final profiles = + ref + .watch(profilesProvider) + .valueOrNull ?? + []; + final fromProfiles = profiles + .map( + (p) => p.fullName.isEmpty + ? p.id + : p.fullName, + ) + .where( + (n) => n.toLowerCase().contains( + pattern.toLowerCase(), + ), + ) + .toList(); + try { + final clientRows = await ref + .read(supabaseClientProvider) + .from('clients') + .select('name') + .ilike('name', '%$pattern%'); + final clientNames = + (clientRows as List?) + ?.map( + (r) => + r['name'] as String, + ) + .whereType() + .toList() ?? + []; + final merged = { + ...fromProfiles, + ...clientNames, + }.toList(); + return merged; + } catch (_) { + return fromProfiles; + } + }, + itemBuilder: (context, suggestion) => + ListTile(title: Text(suggestion)), + onSuggestionSelected: + (suggestion) async { + _notedDebounce?.cancel(); + _notedController.text = + suggestion; + setState(() { + _notedSaving = true; + _notedSaved = false; + }); + try { + await ref + .read( + tasksControllerProvider, + ) + .updateTask( + taskId: task.id, + notedBy: + suggestion.isEmpty + ? null + : suggestion, + ); + 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, + ); + } + }, + ); + } + } + }, + ), + + const SizedBox(height: 12), + Text( + 'Received by', + style: Theme.of( + context, + ).textTheme.bodySmall, + ), + const SizedBox(height: 6), + TypeAheadFormField( + textFieldConfiguration: TextFieldConfiguration( + controller: _receivedController, + decoration: InputDecoration( + hintText: 'Receiver name or id', + suffixIcon: _receivedSaving + ? SizedBox( + width: 16, + height: 16, + child: ScaleTransition( + scale: _savePulse, + child: const Icon( + Icons.save, + size: 14, + ), + ), + ) + : _receivedSaved + ? SizedBox( + width: 16, + height: 16, + child: Stack( + alignment: + Alignment.center, + children: const [ + Icon( + Icons.save, + size: 14, + color: Colors.green, + ), + Positioned( + right: -2, + bottom: -2, + child: Icon( + Icons.check, + size: 10, + color: Colors.white, + ), + ), + ], + ), + ) + : null, + ), + onChanged: (v) { + _receivedDebounce?.cancel(); + _receivedDebounce = Timer( + const Duration(milliseconds: 700), + () async { + final name = v.trim(); + setState(() { + _receivedSaving = true; + _receivedSaved = false; + }); + try { + await ref + .read( + tasksControllerProvider, + ) + .updateTask( + taskId: task.id, + receivedBy: name.isEmpty + ? null + : name, + ); + if (name.isNotEmpty) { + try { + await ref + .read( + supabaseClientProvider, + ) + .from('clients') + .upsert({ + 'name': name, + }); + } catch (_) {} + } + setState(() { + _receivedSaved = + name.isNotEmpty; + }); + } catch (_) { + // ignore + } finally { + setState(() { + _receivedSaving = false; + }); + if (_receivedSaved) { + Future.delayed( + const Duration( + seconds: 2, + ), + () { + if (mounted) { + setState( + () => + _receivedSaved = + false, + ); + } + }, + ); + } + } + }, + ); + }, + ), + suggestionsCallback: (pattern) async { + final profiles = + ref + .watch(profilesProvider) + .valueOrNull ?? + []; + final fromProfiles = profiles + .map( + (p) => p.fullName.isEmpty + ? p.id + : p.fullName, + ) + .where( + (n) => n.toLowerCase().contains( + pattern.toLowerCase(), + ), + ) + .toList(); + try { + final clientRows = await ref + .read(supabaseClientProvider) + .from('clients') + .select('name') + .ilike('name', '%$pattern%'); + final clientNames = + (clientRows as List?) + ?.map( + (r) => + r['name'] as String, + ) + .whereType() + .toList() ?? + []; + final merged = { + ...fromProfiles, + ...clientNames, + }.toList(); + return merged; + } catch (_) { + return fromProfiles; + } + }, + itemBuilder: (context, suggestion) => + ListTile(title: Text(suggestion)), + onSuggestionSelected: + (suggestion) async { + _receivedDebounce?.cancel(); + _receivedController.text = + suggestion; + setState(() { + _receivedSaving = true; + _receivedSaved = false; + }); + try { + await ref + .read( + tasksControllerProvider, + ) + .updateTask( + taskId: task.id, + receivedBy: + suggestion.isEmpty + ? null + : suggestion, + ); + 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, + ); + } + }, + ); + } + } + }, + ), + ], ), - ], + ), ), - ), + ], ), - ], - ), + ), + ], ), - ], - ), + ), + ], ), ], );