Files
Kifi/kifi-app/lib/features/projects/presentation/widgets/add_task_sheet.dart

282 lines
10 KiB
Dart

import 'package:flutter/material.dart';
import '../../../../core/widgets/premium_text_field.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import '../../domain/project_task.dart';
import '../../providers/project_provider.dart';
import '../../providers/user_provider.dart';
import '../../../../core/domain/user.dart';
class AddTaskSheet extends ConsumerStatefulWidget {
final int projectId;
final ProjectTask? task;
const AddTaskSheet({super.key, required this.projectId, this.task});
@override
ConsumerState<AddTaskSheet> createState() => _AddTaskSheetState();
}
class _AddTaskSheetState extends ConsumerState<AddTaskSheet> {
final _formKey = GlobalKey<FormState>();
String _title = '';
String _description = '';
String _assigneeName = '';
bool _isBillable = false;
double _hourlyRate = 0;
bool _isLoading = false;
bool get _isEditing => widget.task != null;
@override
void initState() {
super.initState();
if (widget.task != null) {
_title = widget.task!.title;
_description = widget.task!.description ?? '';
_assigneeName = widget.task!.assigneeName ?? '';
_isBillable = widget.task!.isBillable;
_hourlyRate = widget.task!.hourlyRate;
}
}
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
left: 24,
right: 24,
top: 24,
),
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
child: SafeArea(
child: Form(
key: _formKey,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
_isEditing ? 'Edit Task' : 'New Task',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
IconButton(
icon: const Icon(LucideIcons.x),
onPressed: () => Navigator.pop(context),
),
],
),
const SizedBox(height: 16),
PremiumTextField(
labelText: 'Task Title',
initialValue: _isEditing ? _title : null,
prefixIcon: const Icon(LucideIcons.checkSquare),
validator: (val) =>
val == null || val.isEmpty ? 'Required' : null,
onSaved: (val) => _title = val!,
),
const SizedBox(height: 16),
PremiumTextField(
maxLines: 3,
labelText: 'Description (Optional)',
initialValue: _isEditing ? _description : null,
prefixIcon: const Icon(LucideIcons.alignLeft),
onSaved: (val) => _description = val ?? '',
),
const SizedBox(height: 16),
Autocomplete<User>(
initialValue: _isEditing && _assigneeName.isNotEmpty
? TextEditingValue(text: _assigneeName)
: null,
optionsBuilder: (TextEditingValue textEditingValue) async {
if (textEditingValue.text.isEmpty) {
return const Iterable<User>.empty();
}
try {
return await ref.read(
userSearchProvider(textEditingValue.text).future,
);
} catch (e) {
return const Iterable<User>.empty();
}
},
displayStringForOption: (User option) =>
'${option.name} (${option.email})',
onSelected: (User selection) {
_assigneeName = selection.name;
},
optionsViewBuilder: (context, onSelected, options) {
return Align(
alignment: Alignment.topLeft,
child: Material(
elevation: 4,
borderRadius: BorderRadius.circular(12),
color: Colors.white,
child: ConstrainedBox(
constraints: const BoxConstraints(
maxHeight: 200,
maxWidth: 300,
),
child: ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: options.length,
itemBuilder: (BuildContext context, int index) {
final User option = options.elementAt(index);
return ListTile(
title: Text(
option.name,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
subtitle: Text(
option.email,
style: const TextStyle(color: Colors.grey),
),
onTap: () => onSelected(option),
);
},
),
),
),
);
},
fieldViewBuilder:
(
context,
textEditingController,
focusNode,
onFieldSubmitted,
) {
return PremiumTextField(
controller: textEditingController,
focusNode: focusNode,
labelText: 'Assignee Name (Optional)',
prefixIcon: const Icon(LucideIcons.user),
onSaved: (val) {
if (_assigneeName.isEmpty)
_assigneeName = val ?? '';
},
);
},
),
const SizedBox(height: 16),
SwitchListTile(
title: const Text('Is Billable?'),
subtitle: const Text('Task time will be invoiced'),
value: _isBillable,
onChanged: (val) {
setState(() {
_isBillable = val;
});
},
),
if (_isBillable) ...[
const SizedBox(height: 16),
PremiumTextField(
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
),
labelText: 'Hourly Rate',
initialValue: _isEditing && _hourlyRate > 0
? _hourlyRate.toString()
: null,
prefixIcon: const Icon(LucideIcons.indianRupee),
validator: (val) =>
_isBillable && (val == null || val.isEmpty)
? 'Required for billable tasks'
: null,
onSaved: (val) =>
_hourlyRate = double.tryParse(val ?? '0') ?? 0,
),
],
const SizedBox(height: 24),
ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
onPressed: _isLoading ? null : _submit,
child: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: Text(
_isEditing ? 'Save Changes' : 'Create Task',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(height: 24),
],
),
),
),
),
);
}
Future<void> _submit() async {
if (!_formKey.currentState!.validate()) return;
_formKey.currentState!.save();
setState(() => _isLoading = true);
try {
final repo = ref.read(projectRepositoryProvider);
if (_isEditing) {
await repo.updateTask(
taskId: widget.task!.id,
title: _title,
description: _description.isNotEmpty ? _description : null,
assigneeName: _assigneeName.isNotEmpty ? _assigneeName : null,
isBillable: _isBillable,
hourlyRate: _isBillable ? _hourlyRate : null,
);
} else {
await repo.createTask(
projectId: widget.projectId,
title: _title,
description: _description.isNotEmpty ? _description : null,
assigneeName: _assigneeName.isNotEmpty ? _assigneeName : null,
isBillable: _isBillable,
hourlyRate: _isBillable ? _hourlyRate : null,
);
}
ref.invalidate(projectTasksProvider(widget.projectId));
if (mounted) Navigator.pop(context);
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Error: $e')));
}
} finally {
if (mounted) setState(() => _isLoading = false);
}
}
}