naptar-es-feladatkezelo/lib/calendar_widgets.dart

1504 lines
45 KiB
Dart

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<CalendarEvent> events;
final List<Person> people;
final WeatherSnapshot? weather;
final VoidCallback onAdd;
final ValueChanged<CalendarEvent> onEdit;
final void Function(CalendarEvent event, Person person)? onAssignPerson;
@override
State<CalendarPage> createState() => _CalendarPageState();
}
class _CalendarPageState extends State<CalendarPage> {
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<CalendarEvent> events, {
DateTime? now,
}) {
final personal = findNextEventsForPerson(personId, events, 1, now: now);
return personal.isEmpty ? null : personal.first;
}
List<CalendarEvent> findNextEventsForPerson(
String personId,
List<CalendarEvent> 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<Person> 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<CalendarEvent> events;
final List<Person> 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<CalendarEvent> events;
final List<Person> people;
final ValueChanged<CalendarEvent> onEdit;
final void Function(CalendarEvent event, Person person)? onAssignPerson;
final List<CalendarEvent> 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<LiveClockChip> createState() => _LiveClockChipState();
}
class _LiveClockChipState extends State<LiveClockChip> {
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<CalendarEvent> events;
final List<Person> people;
final DateTime weekStart;
final int selectedDay;
final ValueChanged<int> 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<CalendarEvent> events;
final List<Person> 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<CalendarEvent> googleImportedEvents(
List<GoogleCalendarConnection> connections,
) {
final enabledIds = connections
.expand((connection) => connection.enabledCalendarIds)
.toSet();
final imported = <CalendarEvent>[];
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<CalendarEvent> events;
final List<Person> people;
final ValueChanged<CalendarEvent> 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<CalendarEvent> events;
final List<Person> 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<Person>(
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<Person> 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,
),
),
],
),
),
);
}
}