190 lines
9.1 KiB
PHP
190 lines
9.1 KiB
PHP
<?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) : '';
|
|
|
|
/**
|
|
* @return string|null HTTPS embed URL restricted to Google Calendar hosts
|
|
*/
|
|
function familyHub_google_calendar_embed_url(string $raw): ?string {
|
|
$raw = trim($raw);
|
|
if ($raw === '') {
|
|
return null;
|
|
}
|
|
$allowed = static function (string $u): bool {
|
|
$p = parse_url($u);
|
|
if ($p === false || empty($p['scheme']) || empty($p['host'])) {
|
|
return false;
|
|
}
|
|
if (strtolower($p['scheme']) !== 'https') {
|
|
return false;
|
|
}
|
|
$h = strtolower($p['host']);
|
|
if ($h === 'calendar.google.com') {
|
|
return true;
|
|
}
|
|
if ($h === 'www.google.com' && isset($p['path']) && str_starts_with($p['path'], '/calendar')) {
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
if ($allowed($raw)) {
|
|
return $raw;
|
|
}
|
|
if (preg_match('#<iframe\s[^>]*\ssrc\s*=\s*["\']([^"\']+)["\']#i', $raw, $m)) {
|
|
$u = html_entity_decode($m[1], ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
|
return $allowed($u) ? $u : null;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
$embedUrl = familyHub_google_calendar_embed_url($rawEmbed);
|
|
$fromIdOnly = false;
|
|
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']);
|
|
$isTvView = isset($_GET['view']) && (string) $_GET['view'] === 'tv';
|
|
$calendarBaseUrl = '?tab=calendar';
|
|
$calendarTvUrl = '?tab=calendar&view=tv';
|
|
$lastRefreshedStamp = gmdate('Y-m-d H:i') . ' UTC';
|
|
|
|
$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<?= $isTvView ? ' calendar-tv-view' : '' ?>">
|
|
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-2">
|
|
<h2 class="mb-0">Calendar</h2>
|
|
<?php if ($isTvView): ?>
|
|
<a href="<?= htmlspecialchars($calendarBaseUrl, ENT_QUOTES, 'UTF-8') ?>" class="btn btn-outline-secondary btn-sm" title="Exit TV view">
|
|
<i class="fa fa-xmark me-1" aria-hidden="true"></i>
|
|
Exit TV view
|
|
</a>
|
|
<?php else: ?>
|
|
<a href="<?= htmlspecialchars($calendarTvUrl, ENT_QUOTES, 'UTF-8') ?>" class="btn btn-outline-secondary btn-sm" title="Open TV view dashboard">
|
|
<i class="fa fa-tv me-1" aria-hidden="true"></i>
|
|
Open TV view
|
|
</a>
|
|
<?php endif; ?>
|
|
</div>
|
|
<p class="text-muted small mb-3">
|
|
<?php if ($isTvView): ?>
|
|
TV view auto-refreshes every 10 minutes.
|
|
<?php else: ?>
|
|
Open TV view for a dedicated dashboard URL that refreshes every 10 minutes.
|
|
<?php endif; ?>
|
|
</p>
|
|
<?php if ($isTvView): ?>
|
|
<p class="small text-muted mb-3">Last refreshed: <strong><?= htmlspecialchars($lastRefreshedStamp, ENT_QUOTES, 'UTF-8') ?></strong></p>
|
|
<?php endif; ?>
|
|
|
|
<?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 calendar-tab-layout<?= $embedUrl !== null ? ' calendar-tab-layout-split' : '' ?>">
|
|
<div class="col-12 calendar-tab-google">
|
|
<h3 class="h5 mb-3">Google Calendar</h3>
|
|
<?php if ($embedUrl === null): ?>
|
|
<?php if (!$isTvView): ?>
|
|
<p class="text-muted">Connect a shared family calendar by setting <strong>GOOGLE_CALENDAR_EMBED_CODE</strong> (embed URL or full <code><iframe></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 endif; ?>
|
|
<?php else: ?>
|
|
<?php if ($fromIdOnly && !$isTvView): ?>
|
|
<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 class="col-12 calendar-tab-agenda">
|
|
<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>
|
|
</div>
|
|
<?php if ($isTvView): ?>
|
|
<script>
|
|
window.setTimeout(function () {
|
|
window.location.reload();
|
|
}, 10 * 60 * 1000);
|
|
</script>
|
|
<?php endif; ?>
|