part of 'main.dart'; class PageFrame extends StatelessWidget { const PageFrame({ super.key, required this.eyebrow, required this.title, required this.child, this.action, }); final String eyebrow; final String title; final Widget child; final Widget? action; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.fromLTRB(24, 28, 24, 0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Wrap( spacing: 14, runSpacing: 12, crossAxisAlignment: WrapCrossAlignment.end, alignment: WrapAlignment.spaceBetween, children: [ ConstrainedBox( constraints: const BoxConstraints(minWidth: 220, maxWidth: 620), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( eyebrow.toUpperCase(), style: const TextStyle( color: sage, fontSize: 12, letterSpacing: 1.6, fontWeight: FontWeight.w800, ), ), const SizedBox(height: 5), Text( title, style: const TextStyle( color: ink, fontSize: 30, fontWeight: FontWeight.w800, ), ), ], ), ), ?action, ], ), const SizedBox(height: 24), Expanded(child: child), ], ), ); } } class CalendarPage extends StatefulWidget { const CalendarPage({ super.key, required this.events, required this.people, required this.weather, required this.onAdd, required this.onEdit, this.onAssignPerson, }); final List events; final List people; final WeatherSnapshot? weather; final VoidCallback onAdd; final ValueChanged onEdit; final void Function(CalendarEvent event, Person person)? onAssignPerson; @override State createState() => _CalendarPageState(); } class _CalendarPageState extends State { int selectedDay = DateTime.now().weekday - 1; DateTime weekStart = startOfIsoWeek(DateTime.now()); Timer? returnToTodayTimer; static const days = ['H', 'K', 'Sze', 'Cs', 'P', 'Szo', 'V']; @override void dispose() { returnToTodayTimer?.cancel(); super.dispose(); } void _scheduleReturnToToday() { returnToTodayTimer?.cancel(); returnToTodayTimer = Timer(const Duration(seconds: 10), _returnToToday); } void _returnToToday() { final now = DateTime.now(); final currentWeekStart = startOfIsoWeek(now); final currentDay = now.weekday - 1; if (!mounted) return; setState(() { weekStart = currentWeekStart; selectedDay = currentDay; }); } void _selectDay(int day) { setState(() => selectedDay = day); _scheduleReturnToToday(); } void _moveWeek(int weekDelta) { setState(() { weekStart = weekStart.add(Duration(days: weekDelta * 7)); selectedDay = 0; }); _scheduleReturnToToday(); } @override Widget build(BuildContext context) { final selectedDate = weekStart.add(Duration(days: selectedDay)); final todayEvents = widget.events .where((event) => isSameDate(event.date, selectedDate)) .toList() ..sort((a, b) => a.start.compareTo(b.start)); return PageFrame( eyebrow: '${formatDate(weekStart)} \u2013 ${formatDate(weekStart.add(const Duration(days: 6)))}', title: '${isoWeekNumber(weekStart)}. h\u00e9t', action: FilledButton.icon( onPressed: widget.onAdd, icon: const Icon(Icons.add), label: const Text('\u00daj esem\u00e9ny'), ), child: ListView( padding: const EdgeInsets.only(bottom: 24), children: [ LayoutBuilder( builder: (context, constraints) { final heroPanel = CalendarHeroCard( selectedDate: selectedDate, eventCount: todayEvents.length, events: todayEvents, people: widget.people, weather: widget.weather, ); final peoplePanel = NextPersonalEventsTile( events: widget.events, people: widget.people, onEdit: widget.onEdit, ); if (constraints.maxWidth >= 980) { return Column( children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(flex: 5, child: heroPanel), const SizedBox(width: 14), Expanded(flex: 5, child: peoplePanel), ], ), const SizedBox(height: 16), WeekBoard( events: widget.events, people: widget.people, weekStart: weekStart, selectedDay: selectedDay, onSelectDay: _selectDay, onPreviousWeek: () => _moveWeek(-1), onNextWeek: () => _moveWeek(1), ), const SizedBox(height: 16), DayDetailsPanel( selectedDayLabel: days[selectedDay], selectedDate: selectedDate, events: todayEvents, people: widget.people, onEdit: widget.onEdit, onAssignPerson: widget.onAssignPerson, allEvents: widget.events, ), ], ); } return Column( children: [ heroPanel, const SizedBox(height: 14), peoplePanel, const SizedBox(height: 14), WeekBoard( events: widget.events, people: widget.people, weekStart: weekStart, selectedDay: selectedDay, onSelectDay: _selectDay, onPreviousWeek: () => _moveWeek(-1), onNextWeek: () => _moveWeek(1), ), const SizedBox(height: 14), DayDetailsPanel( selectedDayLabel: days[selectedDay], selectedDate: selectedDate, events: todayEvents, people: widget.people, onEdit: widget.onEdit, onAssignPerson: widget.onAssignPerson, allEvents: widget.events, ), ], ); }, ), ], ), ); } } CalendarEvent? findNextEventForPerson( String personId, List events, { DateTime? now, }) { final personal = findNextEventsForPerson(personId, events, 1, now: now); return personal.isEmpty ? null : personal.first; } List findNextEventsForPerson( String personId, List events, int limit, { DateTime? now, }) { final today = startOfDay(now ?? DateTime.now()); final personal = events .where( (e) => e.personId == personId || e.personId == everyonePersonId, ) .where((e) => !startOfDay(e.date).isBefore(today)) .toList() ..sort((a, b) { final dateCompare = a.date.compareTo(b.date); return dateCompare != 0 ? dateCompare : a.start.compareTo(b.start); }); return personal.take(limit).toList(); } Person? personForEvent(CalendarEvent event, List people) { final personId = event.personId; if (personId == null) return null; for (final person in people) { if (person.id == personId) return person; } return null; } bool sameImportedEvent(CalendarEvent a, CalendarEvent b) { if (a.externalId != null && b.externalId != null) { return a.externalId == b.externalId && a.calendarId == b.calendarId; } return a.title == b.title && a.calendarId == b.calendarId && isSameDate(a.date, b.date) && a.start == b.start; } class CalendarHeroCard extends StatelessWidget { const CalendarHeroCard({ super.key, required this.selectedDate, required this.eventCount, required this.events, required this.people, required this.weather, }); final DateTime selectedDate; final int eventCount; final List events; final List people; final WeatherSnapshot? weather; @override Widget build(BuildContext context) { return Container( width: double.infinity, padding: const EdgeInsets.all(18), decoration: BoxDecoration( gradient: const LinearGradient( colors: [Color(0xFF18332D), Color(0xFF2F6B5B), Color(0xFFE89552)], stops: [.05, .66, 1], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(30), boxShadow: [ BoxShadow( color: sage.withValues(alpha: .24), blurRadius: 30, offset: const Offset(0, 18), ), ], ), child: Row( children: [ Expanded( flex: 3, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Ma a csal\u00e1dban', style: TextStyle( color: Colors.white70, fontWeight: FontWeight.w700, ), ), const SizedBox(height: 6), Text( formatDate(selectedDate), style: const TextStyle( color: Colors.white, fontSize: 25, fontWeight: FontWeight.w900, ), ), const SizedBox(height: 7), Text( eventCount == 0 ? 'Szell\u0151s nap. Ilyen is kell.' : '$eventCount program v\u00e1rhat\u00f3 ezen a napon.', style: const TextStyle(color: Colors.white, height: 1.35), ), if (events.isNotEmpty) ...[ const SizedBox(height: 8), ...events.take(3).map((event) { final person = personForEvent(event, people); return Padding( padding: const EdgeInsets.only(bottom: 6), child: Row( children: [ MiniAudienceAvatar(event: event, person: person), const SizedBox(width: 8), Expanded( child: Text( '${event.start} \u00b7 ${event.title}' '${person == null ? '' : ' \u00b7 ${person.name}'}', maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( color: Colors.white, fontWeight: FontWeight.w800, ), ), ), ], ), ); }), ], ], ), ), const SizedBox(width: 14), Expanded( flex: 2, child: Wrap( spacing: 8, runSpacing: 8, alignment: WrapAlignment.end, children: [ const LiveClockChip(), HeroStatChip( icon: weather?.icon ?? Icons.thermostat_outlined, label: weather?.temperatureLabel ?? '\u2014', subtitle: weather == null ? 'id\u0151j\u00e1r\u00e1s' : '${weather!.locationName} \u00b7 ${weather!.description}', ), ], ), ), ], ), ); } } class DayDetailsPanel extends StatelessWidget { const DayDetailsPanel({ super.key, required this.selectedDayLabel, required this.selectedDate, required this.events, required this.people, required this.onEdit, required this.onAssignPerson, required this.allEvents, }); final String selectedDayLabel; final DateTime selectedDate; final List events; final List people; final ValueChanged onEdit; final void Function(CalendarEvent event, Person person)? onAssignPerson; final List allEvents; @override Widget build(BuildContext context) { return Container( width: double.infinity, padding: const EdgeInsets.all(18), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(28), border: Border.all(color: const Color(0xFFE8E5DD)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '$selectedDayLabel, ${formatDate(selectedDate)}', style: const TextStyle( color: ink, fontSize: 20, fontWeight: FontWeight.w900, ), ), const SizedBox(height: 10), if (events.isEmpty) const SizedBox( height: 124, child: EmptyState( icon: Icons.wb_sunny_outlined, title: 'Szabad nap', subtitle: 'Ezen a napon m\u00e9g nincs programod.', ), ) else ...events.map( (event) => Padding( padding: const EdgeInsets.only(bottom: 10), child: EventCard( event: event, person: personForEvent(event, people), onTap: () => onEdit(event), ), ), ), UncategorizedEventsTile( events: allEvents, people: people, onAssignPerson: onAssignPerson, ), ], ), ); } } class LiveClockChip extends StatefulWidget { const LiveClockChip({super.key}); @override State createState() => _LiveClockChipState(); } class _LiveClockChipState extends State { late DateTime now; Timer? timer; @override void initState() { super.initState(); now = DateTime.now(); timer = Timer.periodic(const Duration(seconds: 1), (_) { if (!mounted) return; setState(() => now = DateTime.now()); }); } @override void dispose() { timer?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { return Container( width: 128, constraints: const BoxConstraints(minHeight: 114), padding: const EdgeInsets.all(11), decoration: BoxDecoration( color: Colors.white.withValues(alpha: .18), borderRadius: BorderRadius.circular(22), border: Border.all(color: Colors.white30), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ SizedBox( width: 50, height: 50, child: CustomPaint(painter: _ClockPainter(now)), ), const SizedBox(height: 6), Text( formatTimeWithSeconds(now), style: const TextStyle( color: Colors.white, fontSize: 15, fontWeight: FontWeight.w900, letterSpacing: .4, ), ), const SizedBox(height: 2), const Text( '\u00e9l\u0151 \u00f3ra', style: TextStyle(color: Colors.white70, fontSize: 12), ), ], ), ); } } class _ClockPainter extends CustomPainter { _ClockPainter(this.time); final DateTime time; @override void paint(Canvas canvas, Size size) { final center = Offset(size.width / 2, size.height / 2); final radius = size.shortestSide / 2; final facePaint = Paint() ..color = Colors.white.withValues(alpha: .16) ..style = PaintingStyle.fill; final rimPaint = Paint() ..color = Colors.white.withValues(alpha: .7) ..style = PaintingStyle.stroke ..strokeWidth = 1.5; canvas.drawCircle(center, radius - 1, facePaint); canvas.drawCircle(center, radius - 1, rimPaint); final tickPaint = Paint() ..color = Colors.white.withValues(alpha: .75) ..strokeCap = StrokeCap.round ..strokeWidth = 1.4; for (var i = 0; i < 12; i++) { final angle = math.pi * 2 * i / 12 - math.pi / 2; final outer = center + Offset(math.cos(angle), math.sin(angle)) * (radius - 7); final inner = center + Offset(math.cos(angle), math.sin(angle)) * (radius - 12); canvas.drawLine(inner, outer, tickPaint); } void hand(double angle, double length, double width, Color color) { final paint = Paint() ..color = color ..strokeWidth = width ..strokeCap = StrokeCap.round; final end = center + Offset(math.cos(angle), math.sin(angle)) * length; canvas.drawLine(center, end, paint); } final secondAngle = math.pi * 2 * time.second / 60 - math.pi / 2; final minuteAngle = math.pi * 2 * (time.minute + time.second / 60) / 60 - math.pi / 2; final hourAngle = math.pi * 2 * ((time.hour % 12) + time.minute / 60) / 12 - math.pi / 2; hand(hourAngle, radius * .45, 4, Colors.white); hand(minuteAngle, radius * .62, 3, Colors.white.withValues(alpha: .9)); hand(secondAngle, radius * .68, 1.4, const Color(0xFFFFD08A)); canvas.drawCircle(center, 4, Paint()..color = const Color(0xFFFFD08A)); } @override bool shouldRepaint(covariant _ClockPainter oldDelegate) => oldDelegate.time.second != time.second; } class HeroStatChip extends StatelessWidget { const HeroStatChip({ super.key, required this.icon, required this.label, required this.subtitle, }); final IconData icon; final String label; final String subtitle; @override Widget build(BuildContext context) { return Container( width: 128, constraints: const BoxConstraints(minHeight: 114), padding: const EdgeInsets.all(11), decoration: BoxDecoration( color: Colors.white.withValues(alpha: .15), borderRadius: BorderRadius.circular(22), border: Border.all(color: Colors.white24), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ Icon(icon, color: Colors.white, size: 25), const SizedBox(height: 6), Text( label, style: const TextStyle( color: Colors.white, fontSize: 16, fontWeight: FontWeight.w900, ), ), const SizedBox(height: 2), Text( subtitle, textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(color: Colors.white70, fontSize: 12), ), ], ), ); } } class WeekBoard extends StatelessWidget { const WeekBoard({ super.key, required this.events, required this.people, required this.weekStart, required this.selectedDay, required this.onSelectDay, required this.onPreviousWeek, required this.onNextWeek, }); final List events; final List people; final DateTime weekStart; final int selectedDay; final ValueChanged onSelectDay; final VoidCallback onPreviousWeek; final VoidCallback onNextWeek; static const days = [ 'H\u00e9tf\u0151', 'Kedd', 'Szerda', 'Cs\u00fct\u00f6rt\u00f6k', 'P\u00e9ntek', 'Szombat', 'Vas\u00e1rnap', ]; @override Widget build(BuildContext context) { return Card( child: Padding( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.view_week_outlined, color: sage), const SizedBox(width: 10), const Expanded( child: Text( 'Heti csal\u00e1di napt\u00e1r', style: TextStyle( color: ink, fontSize: 18, fontWeight: FontWeight.w900, ), ), ), IconButton( tooltip: 'El\u0151z\u0151 h\u00e9t', onPressed: onPreviousWeek, icon: const Icon(Icons.chevron_left), ), Text( '${isoWeekNumber(weekStart)}. h\u00e9t', style: const TextStyle( color: sage, fontWeight: FontWeight.w900, ), ), IconButton( tooltip: 'K\u00f6vetkez\u0151 h\u00e9t', onPressed: onNextWeek, icon: const Icon(Icons.chevron_right), ), ], ), const SizedBox(height: 10), Row( crossAxisAlignment: CrossAxisAlignment.start, children: List.generate(7, (index) { final date = weekStart.add(Duration(days: index)); final dayEvents = events .where((event) => isSameDate(event.date, date)) .toList() ..sort((a, b) => a.start.compareTo(b.start)); return Expanded( child: Padding( padding: EdgeInsets.only(right: index == 6 ? 0 : 6), child: WeekDayColumn( title: days[index], dateLabel: formatShortDate(date), active: selectedDay == index, events: dayEvents, people: people, onTap: () => onSelectDay(index), ), ), ); }), ), ], ), ), ); } } class WeekDayColumn extends StatelessWidget { const WeekDayColumn({ super.key, required this.title, required this.dateLabel, required this.active, required this.events, required this.people, required this.onTap, }); final String title; final String dateLabel; final bool active; final List events; final List people; final VoidCallback onTap; @override Widget build(BuildContext context) { return InkWell( borderRadius: BorderRadius.circular(20), onTap: onTap, child: AnimatedContainer( duration: const Duration(milliseconds: 180), width: double.infinity, constraints: const BoxConstraints(minHeight: 190), padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: active ? mint : const Color(0xFFF8F8F5), borderRadius: BorderRadius.circular(18), border: Border.all( color: active ? sage : const Color(0xFFE8E5DD), width: active ? 2 : 1, ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( color: ink, fontSize: 14, fontWeight: FontWeight.w900, ), ), const SizedBox(height: 7), Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 7), alignment: Alignment.center, decoration: BoxDecoration( color: active ? sage : Colors.white, borderRadius: BorderRadius.circular(14), boxShadow: active ? [ BoxShadow( color: sage.withValues(alpha: .18), blurRadius: 12, offset: const Offset(0, 5), ), ] : null, ), child: Text( dateLabel, maxLines: 1, overflow: TextOverflow.fade, softWrap: false, style: TextStyle( color: active ? Colors.white : sage, fontSize: 17, letterSpacing: .5, fontWeight: FontWeight.w900, ), ), ), const SizedBox(height: 10), if (events.isEmpty) const SizedBox( height: 92, child: Center( child: Text( 'nincs program', style: TextStyle( color: Colors.black38, fontWeight: FontWeight.w700, fontSize: 12, ), ), ), ) else ...events .take(4) .map( (event) => WeekEventPill( event: event, person: personForEvent(event, people), ), ), if (events.length > 4) Padding( padding: const EdgeInsets.only(top: 6), child: Text( '+${events.length - 4} tov\u00e1bbi', style: const TextStyle( color: Colors.black54, fontWeight: FontWeight.w800, fontSize: 12, ), ), ), ], ), ), ); } } class WeekEventPill extends StatelessWidget { const WeekEventPill({super.key, required this.event, required this.person}); final CalendarEvent event; final Person? person; @override Widget build(BuildContext context) { final color = person?.color ?? (event.isSynced ? orange : sage); return Container( margin: const EdgeInsets.only(bottom: 6), padding: const EdgeInsets.all(7), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), border: Border(left: BorderSide(color: color, width: 3)), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ MiniAudienceAvatar(event: event, person: person), const SizedBox(width: 6), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( event.start, style: TextStyle( color: color, fontSize: 12, fontWeight: FontWeight.w900, ), ), const SizedBox(height: 1), Text( event.title, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle( color: ink, fontWeight: FontWeight.w800, fontSize: 11.5, height: 1.15, ), ), ], ), ), ], ), ); } } class MiniAudienceAvatar extends StatelessWidget { const MiniAudienceAvatar({ super.key, required this.event, required this.person, }); final CalendarEvent event; final Person? person; @override Widget build(BuildContext context) { if (person != null) { return FamilyAvatar(person: person!, radius: 13); } final icon = event.isSynced ? Icons.inbox_outlined : Icons.groups_2_outlined; final color = event.isSynced ? orange : sage; return CircleAvatar( radius: 13, backgroundColor: color.withValues(alpha: .14), child: Icon(icon, color: color, size: 15), ); } } List googleImportedEvents( List connections, ) { final enabledIds = connections .expand((connection) => connection.enabledCalendarIds) .toSet(); final imported = []; if (enabledIds.contains('primary')) { imported.add( CalendarEvent( 'Google: orvosi kontroll', '14:00', '14:45', const Color(0xFF4285F4), date: DateTime(2026, 7, 23), place: 'Google Calendar', ), ); } if (enabledIds.contains('family')) { imported.add( CalendarEvent( 'Google: k\u00f6z\u00f6s csal\u00e1di eb\u00e9d', '12:30', '14:00', const Color(0xFF34A853), date: DateTime(2026, 7, 26), place: 'Csal\u00e1di napt\u00e1r', ), ); } if (enabledIds.contains('school')) { imported.add( CalendarEvent( 'Google: ovis kir\u00e1ndul\u00e1s', '08:30', '12:00', const Color(0xFFFBBC05), date: DateTime(2026, 7, 24), place: 'Ovi / iskola', ), ); } if (enabledIds.contains('work')) { imported.add( CalendarEvent( 'Google: projekt st\u00e1tusz', '15:00', '15:30', const Color(0xFF9B78B4), date: DateTime(2026, 7, 22), place: 'Munka', ), ); } return imported; } class NextPersonalEventsTile extends StatelessWidget { const NextPersonalEventsTile({ super.key, required this.events, required this.people, required this.onEdit, }); final List events; final List people; final ValueChanged onEdit; @override Widget build(BuildContext context) { return Container( width: double.infinity, padding: const EdgeInsets.all(15), decoration: BoxDecoration( color: mint, borderRadius: BorderRadius.circular(24), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Row( children: [ Icon(Icons.event_available_outlined, color: sage, size: 24), SizedBox(width: 10), Expanded( child: Text( 'Csal\u00e1dtagok k\u00f6vetkez\u0151 esem\u00e9nyei', style: TextStyle( color: ink, fontWeight: FontWeight.w900, fontSize: 19, ), ), ), ], ), const SizedBox(height: 12), if (people.isEmpty) const Text( 'M\u00e9g nincs felvett csal\u00e1dtag.', style: TextStyle(color: Colors.black54), ) else LayoutBuilder( builder: (context, constraints) { if (people.length == 4 && constraints.maxWidth >= 420) { return GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, crossAxisSpacing: 10, mainAxisSpacing: 10, childAspectRatio: 2.05, ), itemCount: people.length, itemBuilder: (_, index) => _personCard(people[index]), ); } return Column( children: [ ...people.map( (person) => Padding( padding: const EdgeInsets.only(bottom: 8), child: _personCard(person), ), ), ], ); }, ), ], ), ); } Widget _personCard(Person person) { final nextEvents = findNextEventsForPerson( person.id, events, person.nextEventLimit, ); return Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(18), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ FamilyAvatar(person: person, radius: 18), const SizedBox(width: 10), Expanded( child: Text( person.name, style: const TextStyle( color: ink, fontSize: 16, fontWeight: FontWeight.w900, ), ), ), Text( '${person.nextEventLimit} db', style: const TextStyle( color: Colors.black45, fontSize: 12, fontWeight: FontWeight.w800, ), ), ], ), const SizedBox(height: 7), if (nextEvents.isEmpty) const Text( 'nincs', style: TextStyle( color: Colors.black45, fontSize: 14, fontWeight: FontWeight.w600, ), ) else ...nextEvents.map( (event) => InkWell( borderRadius: BorderRadius.circular(12), onTap: () => onEdit(event), child: Padding( padding: const EdgeInsets.symmetric(vertical: 3), child: Row( children: [ Expanded( flex: 5, child: Text( event.title, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle( color: ink, fontSize: 15, fontWeight: FontWeight.w900, ), ), ), const SizedBox(width: 8), SizedBox( width: 82, child: Text( '${formatDate(event.date)}\n${event.start}', textAlign: TextAlign.right, style: const TextStyle( color: sage, fontSize: 12, fontWeight: FontWeight.w900, height: 1.2, ), ), ), const SizedBox(width: 4), const Icon( Icons.edit_outlined, size: 15, color: Colors.black38, ), ], ), ), ), ), ], ), ); } } class UncategorizedEventsTile extends StatelessWidget { const UncategorizedEventsTile({ super.key, required this.events, required this.people, required this.onAssignPerson, }); final List events; final List people; final void Function(CalendarEvent event, Person person)? onAssignPerson; @override Widget build(BuildContext context) { final uncategorized = events .where((event) => event.isSynced && event.isUncategorized) .toList() ..sort((a, b) { final dateCompare = a.date.compareTo(b.date); return dateCompare != 0 ? dateCompare : a.start.compareTo(b.start); }); if (uncategorized.isEmpty) return const SizedBox.shrink(); return Padding( padding: const EdgeInsets.only(top: 14), child: Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFFFFF4DF), borderRadius: BorderRadius.circular(20), border: Border.all(color: const Color(0xFFFFD99A)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.inbox_outlined, color: orange), const SizedBox(width: 10), Expanded( child: Text( 'Kategoriz\u00e1latlan esem\u00e9nyek (${uncategorized.length})', style: const TextStyle( color: ink, fontWeight: FontWeight.w900, ), ), ), ], ), const SizedBox(height: 8), const Text( 'Koppints egy esem\u00e9nyre, \u00e9s rendeld csal\u00e1dtaghoz.', style: TextStyle(color: Colors.black54), ), const SizedBox(height: 8), ...uncategorized.map( (event) => Card( margin: const EdgeInsets.only(top: 8), color: Colors.white, child: ListTile( leading: CircleAvatar( backgroundColor: orange.withValues(alpha: .14), child: const Icon(Icons.inbox_outlined, color: orange), ), title: Text( event.title, style: const TextStyle( color: ink, fontWeight: FontWeight.w800, ), ), subtitle: Text( '${formatDate(event.date)} ${event.start}' '${event.calendarName == null ? '' : ' \u00b7 ${event.calendarName}'}', ), trailing: const Icon(Icons.person_add_alt_1_outlined), onTap: people.isEmpty || onAssignPerson == null ? null : () async { final person = await showDialog( context: context, builder: (_) => AssignPersonDialog( event: event, people: people, ), ); if (person != null) { onAssignPerson!(event, person); } }, ), ), ), ], ), ), ); } } class AssignPersonDialog extends StatelessWidget { const AssignPersonDialog({ super.key, required this.event, required this.people, }); final CalendarEvent event; final List people; @override Widget build(BuildContext context) { return AlertDialog( title: const Text('Esem\u00e9ny besorol\u00e1sa'), content: SizedBox( width: 420, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( event.title, style: const TextStyle( color: ink, fontWeight: FontWeight.w900, fontSize: 17, ), ), const SizedBox(height: 4), Text( '${formatDate(event.date)} ${event.start}', style: const TextStyle(color: Colors.black54), ), const SizedBox(height: 16), ...people.map( (person) => Card( child: ListTile( leading: FamilyAvatar(person: person, radius: 18), title: Text(person.name), onTap: () => Navigator.pop(context, person), ), ), ), ], ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('M\u00e9gse'), ), ], ); } } class EventCard extends StatelessWidget { const EventCard({super.key, required this.event, this.person, this.onTap}); final CalendarEvent event; final Person? person; final VoidCallback? onTap; @override Widget build(BuildContext context) { return Card( child: InkWell( borderRadius: BorderRadius.circular(20), onTap: onTap, child: Padding( padding: const EdgeInsets.all(18), child: Row( children: [ Container( width: 5, height: 66, decoration: BoxDecoration( color: event.color, borderRadius: BorderRadius.circular(4), ), ), const SizedBox(width: 16), SizedBox( width: 48, child: Text( event.start, style: const TextStyle( color: ink, fontWeight: FontWeight.w800, fontSize: 15, ), ), ), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( event.title, style: const TextStyle( color: ink, fontWeight: FontWeight.w700, fontSize: 16, ), ), const SizedBox(height: 4), Text( event.place.isEmpty ? '${event.start}\u2013${event.end}' : '${event.start}\u2013${event.end} \u00b7 ${event.place}', style: const TextStyle(color: Colors.black54), ), const SizedBox(height: 6), AudienceChip(person: person, event: event), ], ), ), Icon( event.hasTodos ? Icons.task_alt_rounded : Icons.more_horiz, color: event.hasTodos ? sage : Colors.black38, ), ], ), ), ), ); } } class FamilyAvatar extends StatelessWidget { const FamilyAvatar({super.key, required this.person, this.radius = 22}); final Person person; final double radius; @override Widget build(BuildContext context) { return CircleAvatar( radius: radius, backgroundColor: person.color, child: Text(person.avatar, style: TextStyle(fontSize: radius * .95)), ); } } class AudienceChip extends StatelessWidget { const AudienceChip({super.key, required this.person, required this.event}); final Person? person; final CalendarEvent event; @override Widget build(BuildContext context) { final chipColor = person?.color ?? (event.isUncategorized ? orange : sage); final label = person?.name ?? (event.isUncategorized ? 'Kategoriz\u00e1latlan' : 'Mindenki'); final icon = person != null ? Icons.person_outline : event.isUncategorized ? Icons.inbox_outlined : Icons.groups_2_outlined; return Align( alignment: Alignment.centerLeft, child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( color: chipColor.withValues(alpha: .12), borderRadius: BorderRadius.circular(999), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 14, color: chipColor), const SizedBox(width: 5), Text( label, style: TextStyle( color: chipColor, fontWeight: FontWeight.w800, fontSize: 12, ), ), ], ), ), ); } }