Calendar v2 - Combined Calendar Feature

This commit is contained in:
2026-03-30 16:12:05 -05:00
parent 6353f9c9b4
commit 7eb6160b5a
9 changed files with 672 additions and 34 deletions
+102 -16
View File
@@ -1,4 +1,8 @@
<?php
require_once __DIR__ . '/../includes/family_settings.php';
require_once __DIR__ . '/../includes/calendar_helpers.php';
require_once __DIR__ . '/../includes/utils.php';
$rawEmbed = defined('GOOGLE_CALENDAR_EMBED_CODE') ? trim((string) GOOGLE_CALENDAR_EMBED_CODE) : '';
$calId = defined('GOOGLE_CALENDAR_ID') ? trim((string) GOOGLE_CALENDAR_ID) : '';
@@ -43,25 +47,107 @@ if ($embedUrl === null && $calId !== '' && $calId !== 'your_calendar_id_here') {
$embedUrl = 'https://calendar.google.com/calendar/embed?src=' . rawurlencode($calId);
$fromIdOnly = true;
}
[$rangeStart, $rangeEnd] = hubCalendarDefaultAgendaRange($familySettings);
$events = hubCalendarAgendaEvents($rangeStart, $rangeEnd, $people, $familySettings);
[, $tzLocal] = familyHubCalendarContext($familySettings);
$todayYmd = familyHubTodayYmdInTz($tzLocal);
$twoWayMerge = !empty($familySettings['calendar_two_way_google']);
$eventsByDate = [];
foreach ($events as $ev) {
$d = (string) ($ev['date'] ?? '');
if ($d === '') {
continue;
}
if (!isset($eventsByDate[$d])) {
$eventsByDate[$d] = [];
}
$eventsByDate[$d][] = $ev;
}
ksort($eventsByDate);
$iconByType = [
'chore' => 'fa-tasks',
'meal' => 'fa-utensils',
'grocery_due' => 'fa-cart-shopping',
'expense' => 'fa-coins',
'bill_day' => 'fa-file-invoice-dollar',
];
?>
<div id="calendar" class="tab-content">
<h2 class="mb-2">Calendar</h2>
<?php if ($embedUrl === null): ?>
<p class="text-muted">Connect a shared family calendar by setting <strong>GOOGLE_CALENDAR_EMBED_CODE</strong> (embed URL or full <code>&lt;iframe&gt;</code> from Google Calendar) or <strong>GOOGLE_CALENDAR_ID</strong> in <code>.env</code>. See <a href="https://calendar.google.com/">Google Calendar</a> → calendar menu → <strong>Settings and sharing</strong> → <strong>Integrate calendar</strong>.</p>
<?php else: ?>
<?php if ($fromIdOnly): ?>
<p class="small text-muted mb-2">Showing calendar from <code>GOOGLE_CALENDAR_ID</code>. For more control (view, height), paste the embed URL or iframe into <code>GOOGLE_CALENDAR_EMBED_CODE</code> instead.</p>
<?php endif; ?>
<div class="calendar-embed rounded border overflow-hidden bg-white">
<iframe
class="w-100 border-0 d-block"
style="height: 70vh; min-height: 560px;"
src="<?= htmlspecialchars($embedUrl, ENT_QUOTES, 'UTF-8') ?>"
loading="lazy"
title="Family calendar"
allowfullscreen
></iframe>
</div>
<?php if ($twoWayMerge): ?>
<div class="alert alert-info mb-3" role="status">
<strong>Two-way Google Calendar sync</strong> is enabled in Family settings. OAuth connection is not available in this version yet. Your Google view below (if configured) and the Family Hub agenda still work; full sync will require signing in with Google when supported.
</div>
<?php endif; ?>
<div class="row g-4">
<div class="col-12 <?= $embedUrl !== null ? 'col-xl-7' : '' ?>">
<h3 class="h5 mb-3">Family Hub agenda</h3>
<p class="text-muted small mb-3">
<?= htmlspecialchars($rangeStart, ENT_QUOTES, 'UTF-8') ?> to <?= htmlspecialchars($rangeEnd, ENT_QUOTES, 'UTF-8') ?>
· Times use <strong><?= htmlspecialchars(str_replace('_', ' ', (string) ($familySettings['timezone'] ?? '')), ENT_QUOTES, 'UTF-8') ?></strong>
</p>
<?php if ($eventsByDate === []): ?>
<p class="text-muted mb-0">Nothing scheduled in this range. Add chores, meal plan slots, expenses, or bill reminders.</p>
<?php else: ?>
<div class="list-group calendar-agenda-list">
<?php foreach ($eventsByDate as $dateStr => $dayEvents): ?>
<div class="list-group-item calendar-agenda-day px-0 py-2<?= $dateStr === $todayYmd ? ' calendar-agenda-today border-primary border' : '' ?>">
<div class="px-3 pb-1 small fw-semibold text-muted calendar-agenda-date-label">
<?php if ($dateStr === $todayYmd): ?>
<span class="text-primary">Today</span> ·
<?php endif; ?>
<?= htmlspecialchars($dateStr, ENT_QUOTES, 'UTF-8') ?>
<?php if ($dateStr < $todayYmd): ?>
<span class="badge bg-secondary ms-1">Past</span>
<?php endif; ?>
</div>
<?php foreach ($dayEvents as $ev): ?>
<?php
$t = (string) ($ev['type'] ?? '');
$ic = $iconByType[$t] ?? 'fa-circle';
$href = '?' . (string) ($ev['hrefQuery'] ?? 'tab=calendar');
$det = trim((string) ($ev['detail'] ?? ''));
?>
<a href="<?= htmlspecialchars($href, ENT_QUOTES, 'UTF-8') ?>" class="list-group-item list-group-item-action d-flex align-items-start gap-3 py-3 border-0 border-top rounded-0">
<span class="fa <?= htmlspecialchars($ic, ENT_QUOTES, 'UTF-8') ?> fa-fw text-muted mt-1" aria-hidden="true"></span>
<span class="flex-grow-1 min-w-0">
<span class="d-block fw-medium"><?= sanitizeInput((string) ($ev['title'] ?? '')) ?></span>
<?php if ($det !== ''): ?>
<span class="d-block small text-muted text-break"><?= sanitizeInput($det) ?></span>
<?php endif; ?>
</span>
</a>
<?php endforeach; ?>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<div class="col-12 <?= $embedUrl !== null ? 'col-xl-5' : '' ?>">
<h3 class="h5 mb-3">Google Calendar</h3>
<?php if ($embedUrl === null): ?>
<p class="text-muted">Connect a shared family calendar by setting <strong>GOOGLE_CALENDAR_EMBED_CODE</strong> (embed URL or full <code>&lt;iframe&gt;</code> from Google Calendar) or <strong>GOOGLE_CALENDAR_ID</strong> in <code>.env</code>. See <a href="https://calendar.google.com/">Google Calendar</a> → calendar menu → <strong>Settings and sharing</strong> → <strong>Integrate calendar</strong>.</p>
<?php else: ?>
<?php if ($fromIdOnly): ?>
<p class="small text-muted mb-2">Showing calendar from <code>GOOGLE_CALENDAR_ID</code>. For more control (view, height), paste the embed URL or iframe into <code>GOOGLE_CALENDAR_EMBED_CODE</code> instead.</p>
<?php endif; ?>
<div class="calendar-embed rounded border overflow-hidden bg-white">
<iframe
class="w-100 border-0 d-block"
style="height: 70vh; min-height: 360px;"
src="<?= htmlspecialchars($embedUrl, ENT_QUOTES, 'UTF-8') ?>"
loading="lazy"
title="Family calendar"
allowfullscreen
></iframe>
</div>
<?php endif; ?>
</div>
</div>
</div>
+75 -7
View File
@@ -3,6 +3,13 @@ require_once __DIR__ . '/../includes/db.php';
require_once __DIR__ . '/../includes/utils.php';
require_once __DIR__ . '/../includes/persona.php';
require_once __DIR__ . '/../includes/grocery_helpers.php';
require_once __DIR__ . '/../includes/family_settings.php';
$usTimezones = familyHubUsTimezoneIdentifiers();
$billDays = $familySettings['calendar_bill_days'] ?? [];
if (!is_array($billDays) || $billDays === []) {
$billDays = [['dayOfMonth' => 1, 'title' => '']];
}
$canManage = count($people) === 0
|| (
@@ -54,6 +61,73 @@ $permanenceOptions = [
</div>
<?php endif; ?>
<div class="card mb-4">
<div class="card-header">Calendar and dates</div>
<div class="card-body">
<p class="text-muted small">Used for the <strong>Calendar</strong> tab agenda (today, due dates, and bill reminders). US timezones only.</p>
<?php if (!$canManage && count($people) > 0): ?>
<p class="text-muted">Switch to a verified Head of household to edit these settings.</p>
<?php endif; ?>
<form id="calendarSettingsForm" class="row g-3">
<div class="col-md-6">
<label class="form-label" for="family_timezone">Family timezone</label>
<select class="form-select" id="family_timezone" name="timezone" <?= !$canManage ? 'disabled' : '' ?>>
<?php
$curTz = (string) ($familySettings['timezone'] ?? 'America/New_York');
foreach ($usTimezones as $zid):
?>
<option value="<?= htmlspecialchars($zid, ENT_QUOTES, 'UTF-8') ?>" <?= $curTz === $zid ? 'selected' : '' ?>>
<?= htmlspecialchars(str_replace('_', ' ', $zid), ENT_QUOTES, 'UTF-8') ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-6 d-flex align-items-end">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" id="calendar_two_way_google" name="calendar_two_way_google" value="1"
<?= !empty($familySettings['calendar_two_way_google']) ? 'checked' : '' ?>
<?= !$canManage ? 'disabled' : '' ?>>
<label class="form-check-label" for="calendar_two_way_google">Enable two-way Google Calendar sync</label>
</div>
</div>
<div class="col-12">
<label class="form-label">Monthly bill reminders</label>
<p class="small text-muted mb-2">Shown on the family agenda on the same day each month (e.g. rent). Leave a row blank or clear the title to skip.</p>
<div id="billDaysRows" class="d-flex flex-column gap-2">
<?php foreach ($billDays as $i => $bd): ?>
<?php
$dom = (int) ($bd['dayOfMonth'] ?? 1);
$bt = (string) ($bd['title'] ?? '');
?>
<div class="bill-day-row row g-2 align-items-end">
<div class="col-4 col-md-3">
<label class="form-label small text-muted">Day of month</label>
<select class="form-select bill-day-dom" aria-label="Bill day of month">
<?php for ($d = 1; $d <= 31; $d++): ?>
<option value="<?= $d ?>" <?= $dom === $d ? 'selected' : '' ?>><?= $d ?></option>
<?php endfor; ?>
</select>
</div>
<div class="col">
<label class="form-label small text-muted">Reminder title</label>
<input type="text" class="form-control bill-day-title" maxlength="120" placeholder="e.g. Rent" value="<?= htmlspecialchars($bt, ENT_QUOTES, 'UTF-8') ?>">
</div>
</div>
<?php endforeach; ?>
</div>
<?php if ($canManage): ?>
<button type="button" class="btn btn-outline-secondary btn-sm mt-2" id="btnAddBillDayRow">Add reminder row</button>
<div class="col-12 mt-3">
<button type="submit" class="btn btn-primary">Save calendar settings</button>
</div>
<?php endif; ?>
<div class="col-12">
<div class="alert d-none" id="calendarSettingsFeedback" role="status"></div>
</div>
</form>
</div>
</div>
<div class="card mb-4">
<div class="card-header">Chore economy</div>
<div class="card-body">
@@ -83,12 +157,6 @@ $permanenceOptions = [
<?php endforeach; ?>
</select>
</div>
<div class="col-md-6">
<label class="form-label" for="timezone">Timezone</label>
<input class="form-control" id="timezone" name="timezone"
value="<?= sanitizeInput($familySettings['timezone'] ?? 'UTC') ?>"
<?= !$canManage ? 'disabled' : '' ?>>
</div>
<div class="col-md-6">
<label class="form-label" for="week_starts_on">Week starts on (0 = Sunday)</label>
<input type="number" class="form-control" id="week_starts_on" name="week_starts_on" min="0" max="6"
@@ -97,7 +165,7 @@ $permanenceOptions = [
</div>
<?php if ($canManage): ?>
<div class="col-12">
<button type="submit" class="btn btn-primary">Save settings</button>
<button type="submit" class="btn btn-primary">Save chore economy</button>
</div>
<?php endif; ?>
<div class="col-12">