Project management related changes

This commit is contained in:
2026-08-24 22:57:06 +05:30
parent 71b2389221
commit 2a106a8716
67 changed files with 3565 additions and 80 deletions

View File

@@ -0,0 +1,225 @@
import 'package:flutter/material.dart';
import '../../../../core/widgets/premium_text_field.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lucide_icons/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);
}
}
}