simplified code across the app and consolidated duplicated helpers
This commit is contained in:
@@ -13,6 +13,7 @@ import '../../../../widget/async_action_button.dart';
|
||||
import '../../../../widget/demo_restricted.dart';
|
||||
import '../../../../widget/focus_behaviour.dart';
|
||||
import '../data/subject_color_palette.dart';
|
||||
import '../data/timetable_appointment_factory.dart';
|
||||
import '../subject_colors/subject_color_picker.dart';
|
||||
|
||||
class CustomEventEditDialog extends StatefulWidget {
|
||||
@@ -72,9 +73,10 @@ class _CustomEventEditDialogState extends State<CustomEventEditDialog> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (_isEditing) {
|
||||
final s = widget.existingEvent!.startDate;
|
||||
final e = widget.existingEvent!.endDate;
|
||||
_isAllDay = isAllDayConvention(s, e);
|
||||
final existing = widget.existingEvent!;
|
||||
final s = existing.startDate;
|
||||
final e = existing.endDate;
|
||||
_isAllDay = TimetableAppointmentFactory.isCustomEventAllDay(existing);
|
||||
if (_isAllDay) {
|
||||
_startTime = _defaultStart;
|
||||
_endTime = _defaultEnd;
|
||||
@@ -119,21 +121,6 @@ class _CustomEventEditDialogState extends State<CustomEventEditDialog> {
|
||||
return (fromMin(start), fromMin(end));
|
||||
}
|
||||
|
||||
/// 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;
|
||||
|
||||
Future<void> _save() async {
|
||||
if (guardDemoAction(context)) return;
|
||||
if (_name.text.trim().isEmpty) {
|
||||
@@ -142,7 +129,7 @@ class _CustomEventEditDialogState extends State<CustomEventEditDialog> {
|
||||
|
||||
// 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 midnight = _date.dateOnly;
|
||||
final startDate = _isAllDay ? midnight : _date.withTime(_startTime);
|
||||
final endDate = _isAllDay ? midnight : _date.withTime(_endTime);
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ bool isOutsideSchoolHours(Appointment a) {
|
||||
}
|
||||
|
||||
int dayIndex(DateTime t, DateTime weekStart) =>
|
||||
DateTime(t.year, t.month, t.day).difference(weekStart).inDays;
|
||||
t.dateOnly.difference(weekStart).inDays;
|
||||
|
||||
class BoundRegion {
|
||||
final TimeRegion region;
|
||||
@@ -41,7 +41,7 @@ class BoundRegion {
|
||||
|
||||
List<BoundRegion> expandRegionsForDay(List<TimeRegion> regions, DateTime day) {
|
||||
final result = <BoundRegion>[];
|
||||
final dayStart = DateTime(day.year, day.month, day.day);
|
||||
final dayStart = day.dateOnly;
|
||||
for (final region in regions) {
|
||||
final isRecurringDaily =
|
||||
region.recurrenceRule != null &&
|
||||
@@ -123,11 +123,7 @@ partitionAppointmentsForWeek(
|
||||
)) {
|
||||
continue;
|
||||
}
|
||||
final idx = DateTime(
|
||||
occLocal.year,
|
||||
occLocal.month,
|
||||
occLocal.day,
|
||||
).difference(weekStart).inDays;
|
||||
final idx = dayIndex(occLocal, weekStart);
|
||||
if (idx < 0 || idx >= 5) continue;
|
||||
final newStart = DateTime(
|
||||
occLocal.year,
|
||||
|
||||
@@ -5,6 +5,7 @@ import '../../../../api/marianumconnect/models/mc_holiday.dart';
|
||||
import '../../../../api/marianumconnect/queries/timetable_get_subjects/timetable_get_subjects_response.dart';
|
||||
import '../../../../api/marianumconnect/queries/timetable_get_week/timetable_get_week_response.dart';
|
||||
import '../../../../api/mhsl/custom_timetable_event/custom_timetable_event.dart';
|
||||
import '../../../../extensions/date_time.dart';
|
||||
import '../../../../storage/timetable_settings.dart';
|
||||
import 'arbitrary_appointment.dart';
|
||||
import 'lesson_color.dart';
|
||||
@@ -150,16 +151,8 @@ class TimetableAppointmentFactory {
|
||||
List<DateTime> _holidayExceptionDates(CustomTimetableEvent event) {
|
||||
final result = <DateTime>[];
|
||||
for (final holiday in holidays) {
|
||||
var day = DateTime(
|
||||
holiday.startDate.year,
|
||||
holiday.startDate.month,
|
||||
holiday.startDate.day,
|
||||
);
|
||||
final last = DateTime(
|
||||
holiday.endDate.year,
|
||||
holiday.endDate.month,
|
||||
holiday.endDate.day,
|
||||
);
|
||||
var day = holiday.startDate.dateOnly;
|
||||
final last = holiday.endDate.dateOnly;
|
||||
while (!day.isAfter(last)) {
|
||||
result.add(
|
||||
DateTime(
|
||||
@@ -171,7 +164,7 @@ class TimetableAppointmentFactory {
|
||||
event.startDate.second,
|
||||
),
|
||||
);
|
||||
day = DateTime(day.year, day.month, day.day + 1);
|
||||
day = day.addDays(1);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
||||
@@ -54,11 +54,8 @@ class CustomEventSheet {
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
showDeleteCustomEventDialog(context, event).future.then((
|
||||
_,
|
||||
) {
|
||||
if (!sheetCtx.mounted) return;
|
||||
Navigator.of(sheetCtx).pop();
|
||||
showDeleteCustomEventDialog(context, event).then((_) {
|
||||
if (sheetCtx.mounted) Navigator.of(sheetCtx).pop();
|
||||
});
|
||||
},
|
||||
label: const Text('Löschen'),
|
||||
|
||||
@@ -8,12 +8,13 @@ import '../../../../state/app/modules/timetable/bloc/timetable_bloc.dart';
|
||||
import '../../../../widget/confirm_dialog.dart';
|
||||
import '../../../../widget/demo_restricted.dart';
|
||||
|
||||
Completer<void> showDeleteCustomEventDialog(
|
||||
/// Completes once the event was deleted; never when the dialog is dismissed.
|
||||
Future<void> showDeleteCustomEventDialog(
|
||||
BuildContext context,
|
||||
CustomTimetableEvent event,
|
||||
) {
|
||||
final completer = Completer<void>();
|
||||
if (guardDemoAction(context)) return completer;
|
||||
if (guardDemoAction(context)) return completer.future;
|
||||
final bloc = context.read<TimetableBloc>();
|
||||
ConfirmDialog(
|
||||
title: 'Termin löschen',
|
||||
@@ -25,5 +26,5 @@ Completer<void> showDeleteCustomEventDialog(
|
||||
completer.complete();
|
||||
},
|
||||
).asDialog(context);
|
||||
return completer;
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncfusion_flutter_calendar/calendar.dart';
|
||||
|
||||
import '../../../../api/marianumconnect/queries/timetable_get_element_week/timetable_element_type.dart';
|
||||
import '../../../../api/marianumconnect/queries/timetable_get_subjects/timetable_get_subjects_response.dart';
|
||||
import '../../../../api/marianumconnect/queries/timetable_get_week/timetable_get_week_response.dart';
|
||||
import '../../../../extensions/date_time.dart';
|
||||
import '../../../../extensions/text.dart';
|
||||
@@ -31,12 +32,11 @@ class LessonSheet {
|
||||
// ändern, ohne dass sich der fremde Plan aktualisiert — daher nur im
|
||||
// eigenen Plan anbieten. Die Einfärbung (eigene Farben) bleibt.
|
||||
final bloc = context.read<TimetableBloc>();
|
||||
final subjects = state.subjects?.result ?? const <McSubject>[];
|
||||
McSubject? subjectFor(String shortName) =>
|
||||
subjects.where((s) => s.shortName == shortName).firstOrNull;
|
||||
final subjectShort = lesson.subjects.firstOrNull;
|
||||
final subjectEntry = subjectShort == null
|
||||
? null
|
||||
: state.subjects?.result
|
||||
.where((s) => s.shortName == subjectShort)
|
||||
.firstOrNull;
|
||||
final subjectEntry = subjectShort == null ? null : subjectFor(subjectShort);
|
||||
final headerLong = subjectEntry?.longName;
|
||||
// Bei Stunden ohne Fach (Pausenaufsicht etc.) den Lesson-Type-Titel
|
||||
// einsetzen — sonst stünde im Header nur ein generisches "?".
|
||||
@@ -85,9 +85,7 @@ class LessonSheet {
|
||||
icon: Icons.book_outlined,
|
||||
label: 'Fächer',
|
||||
entries: lesson.subjects
|
||||
.map(
|
||||
(s) => _line(s, longname: _subjectLongName(state.subjects, s)),
|
||||
)
|
||||
.map((s) => _line(s, longname: subjectFor(s)?.longName))
|
||||
.toList(),
|
||||
),
|
||||
_roomTile(context, lesson),
|
||||
@@ -120,14 +118,10 @@ class LessonSheet {
|
||||
);
|
||||
}
|
||||
|
||||
final entries = lesson.rooms
|
||||
.map((name) => (main: _line(name), sub: null as String?))
|
||||
.toList();
|
||||
|
||||
return _listTileWithSubs(
|
||||
return _listTile(
|
||||
icon: Icons.room,
|
||||
label: lesson.rooms.length == 1 ? 'Raum' : 'Räume',
|
||||
entries: entries,
|
||||
entries: lesson.rooms.map(_line).toList(),
|
||||
trailing: trailing,
|
||||
);
|
||||
}
|
||||
@@ -229,43 +223,6 @@ class LessonSheet {
|
||||
);
|
||||
}
|
||||
|
||||
static Widget _listTileWithSubs({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required List<({String main, String? sub})> entries,
|
||||
Widget? trailing,
|
||||
}) {
|
||||
if (entries.length == 1) {
|
||||
final e = entries.first;
|
||||
return ListTile(
|
||||
leading: Icon(icon),
|
||||
title: Text('$label: ${e.main}'),
|
||||
subtitle: e.sub != null ? Text(e.sub!) : null,
|
||||
trailing: trailing,
|
||||
);
|
||||
}
|
||||
return ListTile(
|
||||
leading: Icon(icon),
|
||||
title: Text(label),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: entries
|
||||
.expand<Widget>(
|
||||
(e) => [
|
||||
Text(e.main),
|
||||
if (e.sub != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 12),
|
||||
child: Text(e.sub!),
|
||||
),
|
||||
],
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
trailing: trailing,
|
||||
);
|
||||
}
|
||||
|
||||
static Widget _listTile({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
@@ -347,15 +304,6 @@ class LessonSheet {
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
static String? _subjectLongName(dynamic subjects, String shortName) {
|
||||
if (subjects == null) return null;
|
||||
final list = subjects.result as Iterable<dynamic>;
|
||||
for (final s in list) {
|
||||
if (s.shortName == shortName) return s.longName as String?;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static IconData _iconForStatus(String status) {
|
||||
switch (status) {
|
||||
case 'CANCELLED':
|
||||
|
||||
@@ -10,8 +10,10 @@ import '../../../state/app/modules/foreign_timetable/bloc/foreign_timetable_bloc
|
||||
import '../../../state/app/modules/settings/bloc/settings_cubit.dart';
|
||||
import '../../../state/app/modules/timetable/bloc/timetable_bloc.dart';
|
||||
import '../../../state/app/modules/timetable/bloc/timetable_state.dart';
|
||||
import '../../../state/app/modules/timetable/bloc/timetable_week_loader.dart';
|
||||
import '../../../utils/haptics.dart';
|
||||
import '../../../widget/demo_restricted.dart';
|
||||
import '../foreign_timetable/element_type_icon.dart';
|
||||
import 'custom_events/custom_event_edit_dialog.dart';
|
||||
import 'details/appointment_details_dispatcher.dart';
|
||||
import 'widgets/timetable_calendar_view.dart';
|
||||
@@ -33,14 +35,8 @@ class _TimetableState extends State<Timetable> {
|
||||
/// user's own. Cleared (back to own plan) via the viewing banner.
|
||||
TimetableElementRef? _selected;
|
||||
|
||||
DateTime _initialDisplayDate() => DateTime.now().addDays(2);
|
||||
|
||||
void _jumpToToday() {
|
||||
_calendarKey.currentState?.jumpToToday();
|
||||
}
|
||||
|
||||
bool _isOnInitialWeek(TimetableState state) =>
|
||||
state.startDate == _mondayOf(_initialDisplayDate());
|
||||
static bool _isOnInitialWeek(TimetableState? state) =>
|
||||
state != null && state.startDate == timetableAnchorDate().mondayOfWeek;
|
||||
|
||||
Future<void> _openPicker() async {
|
||||
if (guardDemoAction(context)) return;
|
||||
@@ -57,20 +53,7 @@ class _TimetableState extends State<Timetable> {
|
||||
setState(() => _selected = null);
|
||||
}
|
||||
|
||||
void _onAction(_CalendarAction action) {
|
||||
switch (action) {
|
||||
case _CalendarAction.addEvent:
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) => const CustomEventEditDialog(),
|
||||
barrierDismissible: false,
|
||||
);
|
||||
case _CalendarAction.viewEvents:
|
||||
AppRoutes.openCustomEvents(context);
|
||||
}
|
||||
}
|
||||
|
||||
void _onCreateEventAt(DateTime start, DateTime end) {
|
||||
void _showEventEditor({DateTime? start, DateTime? end}) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) =>
|
||||
@@ -79,6 +62,15 @@ class _TimetableState extends State<Timetable> {
|
||||
);
|
||||
}
|
||||
|
||||
void _onAction(_CalendarAction action) {
|
||||
switch (action) {
|
||||
case _CalendarAction.addEvent:
|
||||
_showEventEditor();
|
||||
case _CalendarAction.viewEvents:
|
||||
AppRoutes.openCustomEvents(context);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final selected = _selected;
|
||||
@@ -87,11 +79,8 @@ class _TimetableState extends State<Timetable> {
|
||||
// (or back to the own plan) tears it down and builds a fresh one.
|
||||
return BlocProvider<ForeignTimetableBloc>(
|
||||
key: ValueKey('${selected.type.name}-${selected.id}'),
|
||||
create: (_) => ForeignTimetableBloc(
|
||||
type: selected.type,
|
||||
elementId: selected.id,
|
||||
title: selected.label,
|
||||
),
|
||||
create: (_) =>
|
||||
ForeignTimetableBloc(type: selected.type, elementId: selected.id),
|
||||
// Builder gives us a context *below* the provider so the foreign bloc is
|
||||
// resolvable inside _buildForeignPlan.
|
||||
child: Builder(
|
||||
@@ -100,55 +89,68 @@ class _TimetableState extends State<Timetable> {
|
||||
);
|
||||
}
|
||||
|
||||
AppBar _appBar({
|
||||
required bool atToday,
|
||||
required bool canViewForeign,
|
||||
bool withEventMenu = false,
|
||||
}) => AppBar(
|
||||
// Der Kalender scrollt nicht (nur ziehen/reloaden), aber seine internen
|
||||
// Scrollables feuern ScrollNotifications, die sonst den Material-3
|
||||
// "scrolled under"-Farbwechsel der AppBar dauerhaft auslösen.
|
||||
notificationPredicate: (_) => false,
|
||||
title: const Text('Stunden & Vertretungsplan'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.home_outlined),
|
||||
tooltip: 'Zur aktuellen Woche',
|
||||
onPressed: atToday
|
||||
? null
|
||||
: () => _calendarKey.currentState?.jumpToToday(),
|
||||
),
|
||||
if (withEventMenu)
|
||||
PopupMenuButton<_CalendarAction>(
|
||||
tooltip: 'Kalendereinträge',
|
||||
icon: const Icon(Icons.edit_calendar_outlined),
|
||||
onSelected: _onAction,
|
||||
itemBuilder: (_) => const [
|
||||
PopupMenuItem(
|
||||
value: _CalendarAction.addEvent,
|
||||
child: ListTile(
|
||||
title: Text('Kalendereintrag hinzufügen'),
|
||||
leading: Icon(Icons.add),
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: _CalendarAction.viewEvents,
|
||||
child: ListTile(
|
||||
title: Text('Kalendereinträge anzeigen'),
|
||||
leading: Icon(Icons.perm_contact_calendar_outlined),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (canViewForeign)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.person_search),
|
||||
tooltip: 'Anderen Stundenplan öffnen',
|
||||
onPressed: _openPicker,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
Widget _buildOwnPlan(BuildContext context) {
|
||||
final bloc = context.read<TimetableBloc>();
|
||||
final loadableState = context.watch<TimetableBloc>().state;
|
||||
final innerState = loadableState.data;
|
||||
final atToday = innerState != null && _isOnInitialWeek(innerState);
|
||||
final atToday = context.select(
|
||||
(TimetableBloc b) => _isOnInitialWeek(b.state.data),
|
||||
);
|
||||
final capabilities = context.watch<CapabilitiesCubit>();
|
||||
final canViewForeign = capabilities.canViewForeignTimetables;
|
||||
final teacherPlan = capabilities.isTeacher;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
// Der Kalender scrollt nicht (nur ziehen/reloaden), aber seine internen
|
||||
// Scrollables feuern ScrollNotifications, die sonst den Material-3
|
||||
// "scrolled under"-Farbwechsel der AppBar dauerhaft auslösen.
|
||||
notificationPredicate: (_) => false,
|
||||
title: const Text('Stunden & Vertretungsplan'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.home_outlined),
|
||||
tooltip: 'Zur aktuellen Woche',
|
||||
onPressed: atToday ? null : _jumpToToday,
|
||||
),
|
||||
PopupMenuButton<_CalendarAction>(
|
||||
tooltip: 'Kalendereinträge',
|
||||
icon: const Icon(Icons.edit_calendar_outlined),
|
||||
onSelected: _onAction,
|
||||
itemBuilder: (_) => const [
|
||||
PopupMenuItem(
|
||||
value: _CalendarAction.addEvent,
|
||||
child: ListTile(
|
||||
title: Text('Kalendereintrag hinzufügen'),
|
||||
leading: Icon(Icons.add),
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: _CalendarAction.viewEvents,
|
||||
child: ListTile(
|
||||
title: Text('Kalendereinträge anzeigen'),
|
||||
leading: Icon(Icons.perm_contact_calendar_outlined),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (canViewForeign)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.person_search),
|
||||
tooltip: 'Anderen Stundenplan öffnen',
|
||||
onPressed: _openPicker,
|
||||
),
|
||||
],
|
||||
appBar: _appBar(
|
||||
atToday: atToday,
|
||||
canViewForeign: canViewForeign,
|
||||
withEventMenu: true,
|
||||
),
|
||||
body: LoadableStateConsumer<TimetableBloc, TimetableState>(
|
||||
// Without this predicate the consumer treats the freshly-initialised
|
||||
@@ -170,7 +172,8 @@ class _TimetableState extends State<Timetable> {
|
||||
teacherPlan: teacherPlan,
|
||||
onOpenElement: canViewForeign ? _openElement : null,
|
||||
),
|
||||
onCreateEvent: _onCreateEventAt,
|
||||
onCreateEvent: (start, end) =>
|
||||
_showEventEditor(start: start, end: end),
|
||||
customEvents: state.customEvents?.events ?? const [],
|
||||
showClassInsteadOfTeacher: teacherPlan,
|
||||
),
|
||||
@@ -180,44 +183,21 @@ class _TimetableState extends State<Timetable> {
|
||||
|
||||
Widget _buildForeignPlan(BuildContext context, TimetableElementRef selected) {
|
||||
final bloc = context.read<ForeignTimetableBloc>();
|
||||
final loadableState = context.watch<ForeignTimetableBloc>().state;
|
||||
final innerState = loadableState.data;
|
||||
final atToday = innerState != null && _isOnInitialWeek(innerState);
|
||||
final canViewForeign = context
|
||||
.watch<CapabilitiesCubit>()
|
||||
.canViewForeignTimetables;
|
||||
final atToday = context.select(
|
||||
(ForeignTimetableBloc b) => _isOnInitialWeek(b.state.data),
|
||||
);
|
||||
final canViewForeign = context.select(
|
||||
(CapabilitiesCubit c) => c.canViewForeignTimetables,
|
||||
);
|
||||
final teacherPlan = selected.type == TimetableElementType.teacher;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
// Siehe _buildOwnPlan: den scroll-under-Farbwechsel unterdrücken, weil
|
||||
// der Kalender nicht scrollt, aber ScrollNotifications feuert.
|
||||
notificationPredicate: (_) => false,
|
||||
title: const Text('Stunden & Vertretungsplan'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.home_outlined),
|
||||
tooltip: 'Zur aktuellen Woche',
|
||||
onPressed: atToday ? null : _jumpToToday,
|
||||
),
|
||||
if (canViewForeign)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.person_search),
|
||||
tooltip: 'Anderen Stundenplan öffnen',
|
||||
onPressed: _openPicker,
|
||||
),
|
||||
],
|
||||
),
|
||||
appBar: _appBar(atToday: atToday, canViewForeign: canViewForeign),
|
||||
body: Column(
|
||||
children: [
|
||||
_ViewingBanner(element: selected, onClose: _backToOwnPlan),
|
||||
Expanded(
|
||||
child: LoadableStateConsumer<ForeignTimetableBloc, TimetableState>(
|
||||
// Foreign plans never carry custom events, so unlike the own-plan
|
||||
// view we must not require `customEvents` here.
|
||||
isReady: (state) =>
|
||||
state.rooms != null &&
|
||||
state.subjects != null &&
|
||||
state.schoolHolidays != null,
|
||||
isReady: (state) => state.hasSchoolReferenceData,
|
||||
child: (state, _) => TimetableCalendarView(
|
||||
key: _calendarKey,
|
||||
state: state,
|
||||
@@ -229,7 +209,6 @@ class _TimetableState extends State<Timetable> {
|
||||
teacherPlan: teacherPlan,
|
||||
onOpenElement: canViewForeign ? _openElement : null,
|
||||
),
|
||||
customEvents: const [],
|
||||
showClassInsteadOfTeacher: teacherPlan,
|
||||
),
|
||||
),
|
||||
@@ -238,11 +217,6 @@ class _TimetableState extends State<Timetable> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static DateTime _mondayOf(DateTime d) {
|
||||
final monday = d.subtractDays(d.weekday - 1);
|
||||
return DateTime(monday.year, monday.month, monday.day);
|
||||
}
|
||||
}
|
||||
|
||||
/// Slim banner shown at the top of the timetable while a foreign element's plan
|
||||
@@ -297,7 +271,7 @@ class _ViewingBanner extends StatelessWidget {
|
||||
padding: const EdgeInsets.fromLTRB(16, 2, 6, 2),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(_iconFor(element.type), size: 16, color: onColor),
|
||||
Icon(element.type.icon, size: 16, color: onColor),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
@@ -324,17 +298,4 @@ class _ViewingBanner extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static IconData _iconFor(TimetableElementType type) {
|
||||
switch (type) {
|
||||
case TimetableElementType.student:
|
||||
return Icons.person_outline;
|
||||
case TimetableElementType.teacher:
|
||||
return Icons.school_outlined;
|
||||
case TimetableElementType.room:
|
||||
return Icons.meeting_room_outlined;
|
||||
case TimetableElementType.schoolClass:
|
||||
return Icons.groups_outlined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,20 +253,8 @@ class _DayColumn extends StatelessWidget {
|
||||
final period = layout.periodAtY(details.localPosition.dy);
|
||||
if (period == null) return;
|
||||
|
||||
final start = DateTime(
|
||||
date.year,
|
||||
date.month,
|
||||
date.day,
|
||||
period.start.hour,
|
||||
period.start.minute,
|
||||
);
|
||||
final end = DateTime(
|
||||
date.year,
|
||||
date.month,
|
||||
date.day,
|
||||
period.end.hour,
|
||||
period.end.minute,
|
||||
);
|
||||
final start = date.withTime(period.start);
|
||||
final end = date.withTime(period.end);
|
||||
if (_overlapsExistingAppointment(start, end, dayAppts)) return;
|
||||
|
||||
Haptics.longPress();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/// Custom 5-day work-week calendar (replaces Syncfusion's `WorkWeek` view).
|
||||
///
|
||||
/// Implementation is split across `calendar/` for readability; everything
|
||||
/// stays in this single library so private widgets and helpers (`_DayColumn`,
|
||||
/// `_PeriodLayout`, `_isAllDayLike`, …) can remain library-private.
|
||||
/// stays in this single library so private widgets (`_DayColumn`,
|
||||
/// `_WeekGrid`, …) can remain library-private.
|
||||
library;
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
@@ -44,11 +44,7 @@ class SpecialRegionsBuilder {
|
||||
for (var day = rangeStart; !day.isAfter(rangeEnd); day = day.addDays(1)) {
|
||||
if (holidayDays.contains(_dayKey(day))) continue;
|
||||
for (final p in breakPeriods) {
|
||||
final start = day.copyWith(
|
||||
hour: p.start.hour,
|
||||
minute: p.start.minute,
|
||||
);
|
||||
breakRegions.add(_breakRegion(start, p.duration));
|
||||
breakRegions.add(_breakRegion(day.withTime(p.start), p.duration));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,16 +60,8 @@ class SpecialRegionsBuilder {
|
||||
// on top of each other.
|
||||
final byDay = <String, _HolidayDay>{};
|
||||
for (final holiday in holidays.result) {
|
||||
final startDay = DateTime(
|
||||
holiday.startDate.year,
|
||||
holiday.startDate.month,
|
||||
holiday.startDate.day,
|
||||
);
|
||||
final endDay = DateTime(
|
||||
holiday.endDate.year,
|
||||
holiday.endDate.month,
|
||||
holiday.endDate.day,
|
||||
);
|
||||
final startDay = holiday.startDate.dateOnly;
|
||||
final endDay = holiday.endDate.dateOnly;
|
||||
// Webuntis treats endDate inclusively (last day of the break) — the
|
||||
// `+ 1` covers single-day public holidays (where startDate == endDate)
|
||||
// and the final day of a multi-day vacation, both of which would
|
||||
|
||||
@@ -7,6 +7,7 @@ import '../../../../extensions/date_time.dart';
|
||||
import '../../../../state/app/modules/capabilities/bloc/capabilities_cubit.dart';
|
||||
import '../../../../state/app/modules/settings/bloc/settings_cubit.dart';
|
||||
import '../../../../state/app/modules/timetable/bloc/timetable_state.dart';
|
||||
import '../../../../state/app/modules/timetable/bloc/timetable_week_loader.dart';
|
||||
import '../data/arbitrary_appointment.dart';
|
||||
import '../data/lesson_period_schedule.dart';
|
||||
import '../data/timetable_appointment_factory.dart';
|
||||
@@ -63,12 +64,10 @@ class TimetableCalendarViewState extends State<TimetableCalendarView> {
|
||||
List<TimeRegion>? _regions;
|
||||
Object? _regionsKey;
|
||||
|
||||
DateTime _initialDisplayDate() => DateTime.now().addDays(2);
|
||||
|
||||
/// Snaps the calendar back to the current week. Exposed so host pages can
|
||||
/// wire it to a "today" AppBar action.
|
||||
void jumpToToday() {
|
||||
_calendarKey.currentState?.jumpToDate(_initialDisplayDate());
|
||||
_calendarKey.currentState?.jumpToDate(timetableAnchorDate());
|
||||
}
|
||||
|
||||
List<Appointment> _appointments(TimetableState state) {
|
||||
@@ -161,7 +160,7 @@ class TimetableCalendarViewState extends State<TimetableCalendarView> {
|
||||
schedule: schedule,
|
||||
appointments: appointments,
|
||||
timeRegions: regions,
|
||||
initialDate: _initialDisplayDate(),
|
||||
initialDate: timetableAnchorDate(),
|
||||
minDate: minDate,
|
||||
maxDate: maxDate,
|
||||
onAppointmentTap: widget.onAppointmentTap,
|
||||
@@ -197,16 +196,14 @@ class TimetableCalendarViewState extends State<TimetableCalendarView> {
|
||||
baseMin = now.subtractDays(14);
|
||||
baseMax = now.addDays(7);
|
||||
}
|
||||
final effectiveMin = state.accessibleStartDate != null
|
||||
? (state.accessibleStartDate!.isAfter(baseMin)
|
||||
? state.accessibleStartDate!
|
||||
: baseMin)
|
||||
: baseMin;
|
||||
final effectiveMax = state.accessibleEndDate != null
|
||||
? (state.accessibleEndDate!.isBefore(baseMax)
|
||||
? state.accessibleEndDate!
|
||||
: baseMax)
|
||||
: baseMax;
|
||||
final accessibleStart = state.accessibleStartDate;
|
||||
final accessibleEnd = state.accessibleEndDate;
|
||||
final effectiveMin = accessibleStart == null
|
||||
? baseMin
|
||||
: _later(accessibleStart, baseMin);
|
||||
final effectiveMax = accessibleEnd == null
|
||||
? baseMax
|
||||
: _earlier(accessibleEnd, baseMax);
|
||||
final today = DateTime.now().dateOnly;
|
||||
final todayMonday = today.mondayOfWeek;
|
||||
final currentWeekEnd = todayMonday.addDays(DateTime.daysPerWeek - 1);
|
||||
@@ -216,12 +213,12 @@ class TimetableCalendarViewState extends State<TimetableCalendarView> {
|
||||
final capMax = futureDays == null
|
||||
? null
|
||||
: _later(today.addDays(futureDays), currentWeekEnd);
|
||||
final cappedMin = capMin != null && effectiveMin.isBefore(capMin)
|
||||
? capMin
|
||||
: effectiveMin;
|
||||
final cappedMax = capMax != null && effectiveMax.isAfter(capMax)
|
||||
? capMax
|
||||
: effectiveMax;
|
||||
final cappedMin = capMin == null
|
||||
? effectiveMin
|
||||
: _later(effectiveMin, capMin);
|
||||
final cappedMax = capMax == null
|
||||
? effectiveMax
|
||||
: _earlier(effectiveMax, capMax);
|
||||
// When the resulting range does not cover the current week — the summer gap
|
||||
// between two school years, or a stale persisted bound — fall back to the
|
||||
// current week, widened to whatever the capabilities still allow. Otherwise
|
||||
|
||||
Reference in New Issue
Block a user