implemented dynamic module settings and configurable bottom bar, added all-day event support to timetable, and overhauled marianum dates UI with month grouping and search
This commit is contained in:
@@ -9,8 +9,8 @@ import 'package:time_range_picker/time_range_picker.dart';
|
||||
import '../../../../api/mhsl/custom_timetable_event/custom_timetable_event.dart';
|
||||
import '../../../../extensions/date_time.dart';
|
||||
import '../../../../state/app/modules/timetable/bloc/timetable_bloc.dart';
|
||||
import '../../../../widget/async_action_button.dart';
|
||||
import '../../../../widget/focus_behaviour.dart';
|
||||
import '../../../../widget/info_dialog.dart';
|
||||
import 'custom_event_colors.dart';
|
||||
|
||||
class CustomEventEditDialog extends StatefulWidget {
|
||||
@@ -34,15 +34,18 @@ class CustomEventEditDialog extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _CustomEventEditDialogState extends State<CustomEventEditDialog> {
|
||||
// Visible window of the timetable / time picker (matches `_pickTimeRange`'s
|
||||
// `disabledTime`). Pre-filled times from outside this window are clamped in.
|
||||
// Selectable window for non-all-day events. Times outside this range are
|
||||
// clamped in. For events outside school hours, use the all-day toggle.
|
||||
static const TimeOfDay _windowStart = TimeOfDay(hour: 8, minute: 0);
|
||||
static const TimeOfDay _windowEnd = TimeOfDay(hour: 16, minute: 30);
|
||||
static const TimeOfDay _defaultStart = _windowStart;
|
||||
static const TimeOfDay _defaultEnd = TimeOfDay(hour: 9, minute: 30);
|
||||
static const int _minDurationMinutes = 15;
|
||||
|
||||
late DateTime _date = widget.existingEvent?.startDate ?? widget.initialStart ?? DateTime.now();
|
||||
late TimeOfDay _startTime;
|
||||
late TimeOfDay _endTime;
|
||||
late bool _isAllDay;
|
||||
late final TextEditingController _name = TextEditingController(
|
||||
text: widget.existingEvent?.title ?? widget.initialTitle,
|
||||
);
|
||||
@@ -61,12 +64,22 @@ class _CustomEventEditDialogState extends State<CustomEventEditDialog> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (_isEditing) {
|
||||
_startTime = widget.existingEvent!.startDate.toTimeOfDay();
|
||||
_endTime = widget.existingEvent!.endDate.toTimeOfDay();
|
||||
final s = widget.existingEvent!.startDate;
|
||||
final e = widget.existingEvent!.endDate;
|
||||
_isAllDay = isAllDayConvention(s, e);
|
||||
if (_isAllDay) {
|
||||
_startTime = _defaultStart;
|
||||
_endTime = _defaultEnd;
|
||||
} else {
|
||||
final clamped = _clampToVisibleWindow(s.toTimeOfDay(), e.toTimeOfDay());
|
||||
_startTime = clamped.$1;
|
||||
_endTime = clamped.$2;
|
||||
}
|
||||
return;
|
||||
}
|
||||
final rawStart = widget.initialStart?.toTimeOfDay() ?? _windowStart;
|
||||
final rawEnd = widget.initialEnd?.toTimeOfDay() ?? const TimeOfDay(hour: 9, minute: 30);
|
||||
_isAllDay = false;
|
||||
final rawStart = widget.initialStart?.toTimeOfDay() ?? _defaultStart;
|
||||
final rawEnd = widget.initialEnd?.toTimeOfDay() ?? _defaultEnd;
|
||||
final clamped = _clampToVisibleWindow(rawStart, rawEnd);
|
||||
_startTime = clamped.$1;
|
||||
_endTime = clamped.$2;
|
||||
@@ -88,17 +101,38 @@ class _CustomEventEditDialogState extends State<CustomEventEditDialog> {
|
||||
return (fromMin(start), fromMin(end));
|
||||
}
|
||||
|
||||
bool _validate() => _name.text.isNotEmpty;
|
||||
/// All-day convention shared with [TimetableAppointmentFactory]: a custom
|
||||
/// event is treated as all-day when its start and end both land on midnight
|
||||
/// of the same day. We piggyback on this so we don't need a backend schema
|
||||
/// change.
|
||||
static bool isAllDayConvention(DateTime start, DateTime end) =>
|
||||
start.year == end.year &&
|
||||
start.month == end.month &&
|
||||
start.day == end.day &&
|
||||
start.hour == 0 &&
|
||||
start.minute == 0 &&
|
||||
start.second == 0 &&
|
||||
end.hour == 0 &&
|
||||
end.minute == 0 &&
|
||||
end.second == 0;
|
||||
|
||||
void _save() {
|
||||
if (!_validate()) return;
|
||||
Future<void> _save() async {
|
||||
if (_name.text.trim().isEmpty) {
|
||||
throw Exception('Bitte einen Terminnamen eingeben.');
|
||||
}
|
||||
|
||||
// All-day convention: store start and end as midnight of the chosen day.
|
||||
// The factory recognises this on read.
|
||||
final midnight = DateTime(_date.year, _date.month, _date.day);
|
||||
final startDate = _isAllDay ? midnight : _date.withTime(_startTime);
|
||||
final endDate = _isAllDay ? midnight : _date.withTime(_endTime);
|
||||
|
||||
final edited = CustomTimetableEvent(
|
||||
id: widget.existingEvent?.id ?? '',
|
||||
title: _name.text,
|
||||
description: _description.text,
|
||||
startDate: _date.withTime(_startTime),
|
||||
endDate: _date.withTime(_endTime),
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
color: _color.name,
|
||||
rrule: _rrule,
|
||||
createdAt: DateTime.now(),
|
||||
@@ -106,17 +140,11 @@ class _CustomEventEditDialogState extends State<CustomEventEditDialog> {
|
||||
);
|
||||
|
||||
final bloc = context.read<TimetableBloc>();
|
||||
final future = _isEditing
|
||||
? bloc.updateCustomEvent(widget.existingEvent!.id, edited)
|
||||
: bloc.addCustomEvent(edited);
|
||||
|
||||
future.then((_) {
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
}).catchError((Object error) {
|
||||
if (!mounted) return;
|
||||
InfoDialog.show(context, error.toString(), copyable: true, title: 'Fehler');
|
||||
});
|
||||
if (_isEditing) {
|
||||
await bloc.updateCustomEvent(widget.existingEvent!.id, edited);
|
||||
} else {
|
||||
await bloc.addCustomEvent(edited);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _pickDate() async {
|
||||
@@ -138,8 +166,8 @@ class _CustomEventEditDialogState extends State<CustomEventEditDialog> {
|
||||
start: _startTime,
|
||||
end: _endTime,
|
||||
disabledTime: TimeRange(
|
||||
startTime: const TimeOfDay(hour: 16, minute: 30),
|
||||
endTime: const TimeOfDay(hour: 8, minute: 0),
|
||||
startTime: _windowEnd,
|
||||
endTime: _windowStart,
|
||||
),
|
||||
disabledColor: Colors.grey,
|
||||
paintingStyle: PaintingStyle.fill,
|
||||
@@ -147,7 +175,7 @@ class _CustomEventEditDialogState extends State<CustomEventEditDialog> {
|
||||
fromText: 'Beginnend',
|
||||
toText: 'Endend',
|
||||
strokeColor: Theme.of(context).colorScheme.secondary,
|
||||
minDuration: const Duration(minutes: 15),
|
||||
minDuration: Duration(minutes: _minDurationMinutes),
|
||||
selectedColor: Theme.of(context).primaryColor,
|
||||
ticks: 24,
|
||||
);
|
||||
@@ -191,12 +219,19 @@ class _CustomEventEditDialogState extends State<CustomEventEditDialog> {
|
||||
subtitle: const Text('Datum'),
|
||||
onTap: _pickDate,
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.access_time_outlined),
|
||||
title: Text('${_startTime.format(context)} - ${_endTime.format(context)}'),
|
||||
subtitle: const Text('Zeitraum'),
|
||||
onTap: _pickTimeRange,
|
||||
SwitchListTile(
|
||||
secondary: const Icon(Icons.today_outlined),
|
||||
title: const Text('Ganztägig'),
|
||||
value: _isAllDay,
|
||||
onChanged: (v) => setState(() => _isAllDay = v),
|
||||
),
|
||||
if (!_isAllDay)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.access_time_outlined),
|
||||
title: Text('${_startTime.format(context)} - ${_endTime.format(context)}'),
|
||||
subtitle: const Text('Zeitraum'),
|
||||
onTap: _pickTimeRange,
|
||||
),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.color_lens_outlined),
|
||||
@@ -246,8 +281,10 @@ class _CustomEventEditDialogState extends State<CustomEventEditDialog> {
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('Abbrechen')),
|
||||
TextButton(onPressed: _save, child: Text(_isEditing ? 'Speichern' : 'Erstellen')),
|
||||
AsyncDialogAction(
|
||||
confirmLabel: _isEditing ? 'Speichern' : 'Erstellen',
|
||||
onConfirm: _save,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user