timetracker/lib/features/projects/presentation/widgets/project_form_sheet.dart
2026-08-03 21:51:48 +02:00

144 lines
4.3 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:timetrack/features/projects/domain/project.dart';
import 'package:timetrack/features/projects/domain/projects_provider.dart';
import 'package:timetrack/features/projects/presentation/widgets/color_picker_row.dart';
class ProjectFormSheet extends ConsumerStatefulWidget {
const ProjectFormSheet({super.key, this.existing});
final Project? existing;
static Future<void> show(BuildContext context, {Project? existing}) {
return showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (_) => ProjectFormSheet(existing: existing),
);
}
@override
ConsumerState<ProjectFormSheet> createState() => _ProjectFormSheetState();
}
class _ProjectFormSheetState extends ConsumerState<ProjectFormSheet> {
late int _color;
final _nameController = TextEditingController();
final _descController = TextEditingController();
String? _error;
@override
void initState() {
super.initState();
_color = widget.existing?.colorValue ?? ColorPickerRow.presetColors.first;
_nameController.text = widget.existing?.name ?? '';
_descController.text = widget.existing?.description ?? '';
}
@override
void dispose() {
_nameController.dispose();
_descController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom + 16,
top: 16,
left: 16,
right: 16,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.existing == null ? 'New Project' : 'Edit Project',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
TextField(
key: const Key('project_name_field'),
controller: _nameController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Name',
),
textCapitalization: TextCapitalization.words,
),
const SizedBox(height: 12),
TextField(
controller: _descController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Description (optional)',
),
),
const SizedBox(height: 16),
Text('Color', style: Theme.of(context).textTheme.labelLarge),
const SizedBox(height: 8),
ColorPickerRow(
selected: _color,
onChanged: (c) => setState(() => _color = c),
),
if (_error != null) ...[
const SizedBox(height: 8),
Text(_error!, style: TextStyle(color: Theme.of(context).colorScheme.error)),
],
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
const SizedBox(width: 8),
FilledButton(
key: const Key('project_save_button'),
onPressed: _save,
child: const Text('Save'),
),
],
),
],
),
);
}
Future<void> _save() async {
final name = _nameController.text.trim();
if (name.isEmpty) {
setState(() => _error = 'Name is required.');
return;
}
final notifier = ref.read(projectsNotifierProvider.notifier);
if (widget.existing == null) {
await notifier.create(
name: name,
colorValue: _color,
description: _descController.text.trim().isEmpty
? null
: _descController.text.trim(),
);
} else {
await notifier.updateProject(widget.existing!.copyWith(
name: name,
colorValue: _color,
description: _descController.text.trim().isEmpty
? null
: _descController.text.trim(),
));
}
if (mounted) Navigator.pop(context);
}
}