Enhance person editing functionality and birthday reminders

- Added a modal for editing person details, including name, role, favorite color, birthday, and description.
- Implemented visibility toggling for PIN input based on role selection, ensuring security for Head of Household.
- Introduced birthday reminder functionality in the calendar, generating events for upcoming birthdays with appropriate notifications.
- Updated calendar and settings UI to accommodate new features, improving user experience and data management.
This commit is contained in:
2026-03-31 18:08:22 -05:00
parent 93ecc5910a
commit df06dcc0d7
6 changed files with 347 additions and 53 deletions
+79 -1
View File
@@ -75,6 +75,30 @@ function calendarBillReminderDatesInRange(
return $out;
}
/**
* Calendar date for month/day in a given year (Feb 29 → Feb 28 on non-leap years).
*/
function hubCalendarBirthdayInYear(int $year, int $month, int $day, DateTimeZone $tzLocal): ?DateTimeImmutable {
if ($month < 1 || $month > 12 || $day < 1 || $day > 31) {
return null;
}
if (!checkdate($month, $day, $year)) {
if ($month === 2 && $day === 29 && checkdate(2, 28, $year)) {
try {
return new DateTimeImmutable(sprintf('%04d-02-28', $year), $tzLocal);
} catch (Exception $e) {
return null;
}
}
return null;
}
try {
return new DateTimeImmutable(sprintf('%04d-%02d-%02d', $year, $month, $day), $tzLocal);
} catch (Exception $e) {
return null;
}
}
/**
* @return int negative if $a sorts before $b
*/
@@ -84,7 +108,7 @@ function hubCalendarEventSortCompare(array $a, array $b): int {
if ($da !== $db) {
return $da <=> $db;
}
$rank = ['chore' => 0, 'meal' => 1, 'grocery_due' => 2, 'expense' => 3, 'bill_day' => 4];
$rank = ['chore' => 0, 'meal' => 1, 'grocery_due' => 2, 'expense' => 3, 'bill_day' => 4, 'birthday_reminder' => 5];
$ta = $rank[(string) ($a['type'] ?? '')] ?? 99;
$tb = $rank[(string) ($b['type'] ?? '')] ?? 99;
if ($ta !== $tb) {
@@ -268,6 +292,60 @@ function hubCalendarAgendaEvents(
}
}
$startYear = (int) substr($rangeStart, 0, 4);
$endYear = (int) substr($rangeEnd, 0, 4);
foreach ($people as $p) {
$nm = trim((string) ($p['name'] ?? ''));
$bdRaw = trim((string) ($p['birthday'] ?? ''));
if ($nm === '' || !preg_match('/^\d{4}-(\d{2})-(\d{2})$/', $bdRaw, $bm)) {
continue;
}
$mm = (int) $bm[1];
$dd = (int) $bm[2];
for ($y = $startYear; $y <= $endYear; $y++) {
$bday = hubCalendarBirthdayInYear($y, $mm, $dd, $tzLocal);
if ($bday === null) {
continue;
}
$bYmd = $bday->format('Y-m-d');
if ($bYmd < $rangeStart || $bYmd > $rangeEnd) {
continue;
}
$idealReminderYmd = $bday->modify('-7 days')->format('Y-m-d');
$eventYmd = max($rangeStart, $idealReminderYmd);
if ($eventYmd > $rangeEnd) {
continue;
}
try {
$evDt = new DateTimeImmutable($eventYmd . ' 00:00:00', $tzLocal);
} catch (Exception $e) {
continue;
}
$interval = $evDt->diff($bday);
if ($interval->invert === 1) {
continue;
}
$daysUntil = (int) $interval->days;
$bdayLabel = $bday->format('F j');
if ($daysUntil === 0) {
$title = $nm . '\'s birthday today';
} elseif ($daysUntil === 1) {
$title = $nm . '\'s birthday tomorrow';
} elseif ($daysUntil === 7) {
$title = $nm . '\'s birthday in one week';
} else {
$title = $nm . '\'s birthday in ' . $daysUntil . ' days';
}
$events[] = [
'type' => 'birthday_reminder',
'date' => $eventYmd,
'title' => $title,
'detail' => 'Birthday on ' . $bdayLabel,
'hrefQuery' => 'tab=settings',
];
}
}
usort($events, 'hubCalendarEventSortCompare');
return $events;
}