Calendar v2 - Combined Calendar Feature
This commit is contained in:
parent
6353f9c9b4
commit
7eb6160b5a
40
api/calendar_events.php
Normal file
40
api/calendar_events.php
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../includes/api_bootstrap.php';
|
||||||
|
require_once __DIR__ . '/../includes/family_settings.php';
|
||||||
|
require_once __DIR__ . '/../includes/calendar_helpers.php';
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
|
||||||
|
sendJson(['success' => false, 'error' => 'Method not allowed'], 405);
|
||||||
|
}
|
||||||
|
|
||||||
|
$people = normalizePeopleList(readJsonFile('people.json'));
|
||||||
|
requireActivePerson($people);
|
||||||
|
|
||||||
|
$familySettings = loadFamilySettings();
|
||||||
|
$from = isset($_GET['from']) ? trim((string) $_GET['from']) : '';
|
||||||
|
$to = isset($_GET['to']) ? trim((string) $_GET['to']) : '';
|
||||||
|
|
||||||
|
if ($from === '' || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $from)) {
|
||||||
|
[$from, $to] = hubCalendarDefaultAgendaRange($familySettings);
|
||||||
|
} else {
|
||||||
|
if ($to === '' || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $to)) {
|
||||||
|
sendJson(['success' => false, 'error' => 'to must be YYYY-MM-DD when from is set'], 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($from > $to) {
|
||||||
|
sendJson(['success' => false, 'error' => 'from must be on or before to'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
[, $tzLocal] = familyHubCalendarContext($familySettings);
|
||||||
|
$today = familyHubTodayYmdInTz($tzLocal);
|
||||||
|
$events = hubCalendarAgendaEvents($from, $to, $people, $familySettings);
|
||||||
|
|
||||||
|
sendJson([
|
||||||
|
'success' => true,
|
||||||
|
'events' => $events,
|
||||||
|
'rangeStart' => $from,
|
||||||
|
'rangeEnd' => $to,
|
||||||
|
'today' => $today,
|
||||||
|
]);
|
||||||
@ -31,7 +31,17 @@ if (isset($body['currency_permanence'])) {
|
|||||||
$merged['currency_permanence'] = $p;
|
$merged['currency_permanence'] = $p;
|
||||||
}
|
}
|
||||||
if (isset($body['timezone'])) {
|
if (isset($body['timezone'])) {
|
||||||
$merged['timezone'] = trim((string) $body['timezone']);
|
$tz = trim((string) $body['timezone']);
|
||||||
|
if (!in_array($tz, familyHubUsTimezoneIdentifiers(), true)) {
|
||||||
|
sendJson(['success' => false, 'error' => 'Invalid timezone'], 400);
|
||||||
|
}
|
||||||
|
$merged['timezone'] = $tz;
|
||||||
|
}
|
||||||
|
if (array_key_exists('calendar_two_way_google', $body)) {
|
||||||
|
$merged['calendar_two_way_google'] = !empty($body['calendar_two_way_google']);
|
||||||
|
}
|
||||||
|
if (isset($body['calendar_bill_days'])) {
|
||||||
|
$merged['calendar_bill_days'] = normalizeCalendarBillDaysRaw($body['calendar_bill_days']);
|
||||||
}
|
}
|
||||||
if (isset($body['week_starts_on'])) {
|
if (isset($body['week_starts_on'])) {
|
||||||
$w = (int) $body['week_starts_on'];
|
$w = (int) $body['week_starts_on'];
|
||||||
@ -41,6 +51,8 @@ if (isset($body['week_starts_on'])) {
|
|||||||
$merged['week_starts_on'] = $w;
|
$merged['week_starts_on'] = $w;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$merged = normalizeLoadedFamilySettings($merged);
|
||||||
|
|
||||||
if (!writeJsonFile('family_settings.json', $merged)) {
|
if (!writeJsonFile('family_settings.json', $merged)) {
|
||||||
sendJson(['success' => false, 'error' => 'Failed to save settings'], 500);
|
sendJson(['success' => false, 'error' => 'Failed to save settings'], 500);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -102,3 +102,11 @@ body {
|
|||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.calendar-agenda-list .calendar-agenda-day:first-child .list-group-item.border-top {
|
||||||
|
border-top: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-agenda-today {
|
||||||
|
background-color: rgba(74, 144, 226, 0.06);
|
||||||
|
}
|
||||||
|
|||||||
@ -179,7 +179,6 @@
|
|||||||
currency_symbol: $('#currency_symbol').val(),
|
currency_symbol: $('#currency_symbol').val(),
|
||||||
currency_name: $('#currency_name').val(),
|
currency_name: $('#currency_name').val(),
|
||||||
currency_permanence: $('#currency_permanence').val(),
|
currency_permanence: $('#currency_permanence').val(),
|
||||||
timezone: $('#timezone').val(),
|
|
||||||
week_starts_on: parseInt($('#week_starts_on').val(), 10)
|
week_starts_on: parseInt($('#week_starts_on').val(), 10)
|
||||||
};
|
};
|
||||||
postJson('/family_settings_save.php', payload)
|
postJson('/family_settings_save.php', payload)
|
||||||
@ -194,6 +193,53 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function collectBillDaysFromDom() {
|
||||||
|
var out = [];
|
||||||
|
$('#billDaysRows .bill-day-row').each(function () {
|
||||||
|
var $row = $(this);
|
||||||
|
var day = parseInt($row.find('.bill-day-dom').val(), 10);
|
||||||
|
var title = ($row.find('.bill-day-title').val() || '').trim();
|
||||||
|
if (title !== '' && day >= 1 && day <= 31) {
|
||||||
|
out.push({ dayOfMonth: day, title: title });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindCalendarSettingsForm() {
|
||||||
|
var $form = $('#calendarSettingsForm');
|
||||||
|
if (!$form.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$('#btnAddBillDayRow').on('click', function () {
|
||||||
|
var $first = $('#billDaysRows .bill-day-row').first();
|
||||||
|
if (!$first.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var $clone = $first.clone();
|
||||||
|
$clone.find('.bill-day-dom').val('1');
|
||||||
|
$clone.find('.bill-day-title').val('');
|
||||||
|
$('#billDaysRows').append($clone);
|
||||||
|
});
|
||||||
|
$form.on('submit', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var payload = {
|
||||||
|
timezone: $('#family_timezone').val(),
|
||||||
|
calendar_two_way_google: $('#calendar_two_way_google').is(':checked'),
|
||||||
|
calendar_bill_days: collectBillDaysFromDom()
|
||||||
|
};
|
||||||
|
postJson('/family_settings_save.php', payload)
|
||||||
|
.then(function () {
|
||||||
|
showAlert($('#calendarSettingsFeedback'), 'success', 'Saved.');
|
||||||
|
$('#calendarSettingsFeedback').removeClass('d-none');
|
||||||
|
})
|
||||||
|
.catch(function (err) {
|
||||||
|
showAlert($('#calendarSettingsFeedback'), 'danger', err.message || 'Could not save');
|
||||||
|
$('#calendarSettingsFeedback').removeClass('d-none');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function choreListPayloadFromForm() {
|
function choreListPayloadFromForm() {
|
||||||
var type = $('#chore_list_type').val() || 'checkbox';
|
var type = $('#chore_list_type').val() || 'checkbox';
|
||||||
var raw = $('#chore_list_items').val() || '';
|
var raw = $('#chore_list_items').val() || '';
|
||||||
@ -785,6 +831,7 @@
|
|||||||
bindFirstHeadForm();
|
bindFirstHeadForm();
|
||||||
bindAddPersonForm();
|
bindAddPersonForm();
|
||||||
bindFamilySettingsForm();
|
bindFamilySettingsForm();
|
||||||
|
bindCalendarSettingsForm();
|
||||||
bindPeopleTable();
|
bindPeopleTable();
|
||||||
bindChoresPage();
|
bindChoresPage();
|
||||||
bindCurrencyPage();
|
bindCurrencyPage();
|
||||||
|
|||||||
289
includes/calendar_helpers.php
Normal file
289
includes/calendar_helpers.php
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/chore_helpers.php';
|
||||||
|
require_once __DIR__ . '/meal_helpers.php';
|
||||||
|
require_once __DIR__ . '/grocery_helpers.php';
|
||||||
|
require_once __DIR__ . '/expense_helpers.php';
|
||||||
|
require_once __DIR__ . '/family_settings.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{0: string, 1: DateTimeZone}
|
||||||
|
*/
|
||||||
|
function familyHubCalendarContext(array $familySettings): array {
|
||||||
|
$allowed = familyHubUsTimezoneIdentifiers();
|
||||||
|
$tzId = normalizeTimezoneToUs((string) ($familySettings['timezone'] ?? ''), $allowed);
|
||||||
|
try {
|
||||||
|
$tz = new DateTimeZone($tzId);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$tz = new DateTimeZone('America/New_York');
|
||||||
|
$tzId = 'America/New_York';
|
||||||
|
}
|
||||||
|
return [$tzId, $tz];
|
||||||
|
}
|
||||||
|
|
||||||
|
function familyHubTodayYmdInTz(DateTimeZone $tzLocal): string {
|
||||||
|
return (new DateTimeImmutable('now', $tzLocal))->format('Y-m-d');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, array{dayOfMonth: int, title: string}> $billDayDefs
|
||||||
|
* @return list<string> Y-m-d dates in range
|
||||||
|
*/
|
||||||
|
function calendarBillReminderDatesInRange(
|
||||||
|
string $rangeStart,
|
||||||
|
string $rangeEnd,
|
||||||
|
array $billDayDefs,
|
||||||
|
DateTimeZone $tzLocal
|
||||||
|
): array {
|
||||||
|
if ($billDayDefs === [] || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $rangeStart) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $rangeEnd)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$start = new DateTimeImmutable($rangeStart . ' 00:00:00', $tzLocal);
|
||||||
|
$end = new DateTimeImmutable($rangeEnd . ' 23:59:59', $tzLocal);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if ($start > $end) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$out = [];
|
||||||
|
$cur = $start->modify('first day of this month')->setTime(0, 0);
|
||||||
|
$lastMonth = $end->modify('first day of this month')->setTime(0, 0);
|
||||||
|
while ($cur <= $lastMonth) {
|
||||||
|
$y = (int) $cur->format('Y');
|
||||||
|
$m = (int) $cur->format('n');
|
||||||
|
$daysInMonth = (int) $cur->format('t');
|
||||||
|
foreach ($billDayDefs as $bd) {
|
||||||
|
$dom = (int) $bd['dayOfMonth'];
|
||||||
|
if ($dom < 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$d = min($dom, $daysInMonth);
|
||||||
|
$ymd = sprintf('%04d-%02d-%02d', $y, $m, $d);
|
||||||
|
try {
|
||||||
|
$dt = new DateTimeImmutable($ymd . ' 00:00:00', $tzLocal);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($dt >= $start && $dt <= $end) {
|
||||||
|
$out[] = $ymd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$cur = $cur->modify('first day of next month');
|
||||||
|
}
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return int negative if $a sorts before $b
|
||||||
|
*/
|
||||||
|
function hubCalendarEventSortCompare(array $a, array $b): int {
|
||||||
|
$da = (string) ($a['date'] ?? '');
|
||||||
|
$db = (string) ($b['date'] ?? '');
|
||||||
|
if ($da !== $db) {
|
||||||
|
return $da <=> $db;
|
||||||
|
}
|
||||||
|
$rank = ['chore' => 0, 'meal' => 1, 'grocery_due' => 2, 'expense' => 3, 'bill_day' => 4];
|
||||||
|
$ta = $rank[(string) ($a['type'] ?? '')] ?? 99;
|
||||||
|
$tb = $rank[(string) ($b['type'] ?? '')] ?? 99;
|
||||||
|
if ($ta !== $tb) {
|
||||||
|
return $ta <=> $tb;
|
||||||
|
}
|
||||||
|
return strcmp((string) ($a['title'] ?? ''), (string) ($b['title'] ?? ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, array<string, mixed>> $people
|
||||||
|
* @param array<string, mixed> $familySettings
|
||||||
|
* @return list<array{type: string, date: string, title: string, detail: string, hrefQuery: string}>
|
||||||
|
*/
|
||||||
|
function hubCalendarAgendaEvents(
|
||||||
|
string $rangeStart,
|
||||||
|
string $rangeEnd,
|
||||||
|
array $people,
|
||||||
|
array $familySettings
|
||||||
|
): array {
|
||||||
|
[$tzId, $tzLocal] = familyHubCalendarContext($familySettings);
|
||||||
|
unset($tzId);
|
||||||
|
|
||||||
|
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $rangeStart) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $rangeEnd)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$events = [];
|
||||||
|
|
||||||
|
$nameById = [];
|
||||||
|
foreach ($people as $p) {
|
||||||
|
if (!empty($p['id'])) {
|
||||||
|
$nameById[(string) $p['id']] = (string) ($p['name'] ?? '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$sym = trim((string) ($familySettings['currency_symbol'] ?? ''));
|
||||||
|
|
||||||
|
$rawChores = readJsonFile('chores.json');
|
||||||
|
$chores = migrateAllChores(normalizeChoresList($rawChores), $people);
|
||||||
|
foreach ($chores as $c) {
|
||||||
|
if (($c['status'] ?? '') !== 'active') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$due = trim((string) ($c['due_date'] ?? ''));
|
||||||
|
if ($due === '' || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $due)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$inForwardWindow = ($due >= $rangeStart && $due <= $rangeEnd);
|
||||||
|
$overdue = ($due < $rangeStart);
|
||||||
|
if (!$inForwardWindow && !$overdue) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$title = trim((string) ($c['title'] ?? ''));
|
||||||
|
if ($title === '') {
|
||||||
|
$title = 'Chore';
|
||||||
|
}
|
||||||
|
$assignees = [];
|
||||||
|
foreach ($c['assignee_ids'] ?? [] as $aid) {
|
||||||
|
$aid = (string) $aid;
|
||||||
|
if ($aid !== '' && isset($nameById[$aid])) {
|
||||||
|
$assignees[] = $nameById[$aid];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$detail = $assignees !== [] ? implode(', ', $assignees) : '';
|
||||||
|
if ($overdue) {
|
||||||
|
$detail = trim('Overdue · ' . $detail, ' ·');
|
||||||
|
}
|
||||||
|
$cid = (string) ($c['id'] ?? '');
|
||||||
|
$events[] = [
|
||||||
|
'type' => 'chore',
|
||||||
|
'date' => $due,
|
||||||
|
'title' => $title,
|
||||||
|
'detail' => $detail,
|
||||||
|
'hrefQuery' => 'tab=chores' . ($cid !== '' ? '&edit=' . rawurlencode($cid) : ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$meals = migrateLegacyMealsList(normalizeMealsList(readJsonFile('meals.json')));
|
||||||
|
$plan = loadMealPlan();
|
||||||
|
$weekStart = (string) ($plan['weekStart'] ?? '');
|
||||||
|
if ($weekStart !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $weekStart)) {
|
||||||
|
foreach (mealSlotTypes() as $slot) {
|
||||||
|
$slotLabel = ucfirst($slot);
|
||||||
|
for ($i = 0; $i < 7; $i++) {
|
||||||
|
$key = (string) $i;
|
||||||
|
$mid = $plan['slots'][$key][$slot] ?? null;
|
||||||
|
if ($mid === null || $mid === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$mid = (string) $mid;
|
||||||
|
$meal = findMealById($meals, $mid);
|
||||||
|
$mTitle = $meal !== null ? trim((string) ($meal['title'] ?? '')) : '';
|
||||||
|
if ($mTitle === '') {
|
||||||
|
$mTitle = 'Meal';
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$d = (new DateTimeImmutable($weekStart . ' 00:00:00', $tzLocal))->modify('+' . $i . ' days')->format('Y-m-d');
|
||||||
|
} catch (Exception $e) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($d < $rangeStart || $d > $rangeEnd) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$events[] = [
|
||||||
|
'type' => 'meal',
|
||||||
|
'date' => $d,
|
||||||
|
'title' => $mTitle,
|
||||||
|
'detail' => $slotLabel,
|
||||||
|
'hrefQuery' => 'tab=meals',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$catalog = normalizeCatalogList(readJsonFile('grocery_catalog.json'));
|
||||||
|
foreach ($catalog as $row) {
|
||||||
|
$nd = isset($row['nextDueDate']) ? trim((string) $row['nextDueDate']) : '';
|
||||||
|
if ($nd === '' || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $nd)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($nd < $rangeStart || $nd > $rangeEnd) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$n = trim((string) ($row['name'] ?? ''));
|
||||||
|
if ($n === '') {
|
||||||
|
$n = 'Grocery item';
|
||||||
|
}
|
||||||
|
$events[] = [
|
||||||
|
'type' => 'grocery_due',
|
||||||
|
'date' => $nd,
|
||||||
|
'title' => $n,
|
||||||
|
'detail' => 'Recurring catalog',
|
||||||
|
'hrefQuery' => 'tab=groceries',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$expenses = normalizeExpensesList(readJsonFile('expenses.json'));
|
||||||
|
foreach ($expenses as $ex) {
|
||||||
|
$d = trim((string) ($ex['date'] ?? ''));
|
||||||
|
if ($d === '' || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $d)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($d < $rangeStart || $d > $rangeEnd) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$t = trim((string) ($ex['title'] ?? ''));
|
||||||
|
if ($t === '') {
|
||||||
|
$t = 'Expense';
|
||||||
|
}
|
||||||
|
$val = isset($ex['value']) && is_numeric($ex['value']) ? round((float) $ex['value'], 2) : null;
|
||||||
|
$aid = (string) ($ex['assignee_id'] ?? '');
|
||||||
|
$an = $aid !== '' ? ($nameById[$aid] ?? '') : '';
|
||||||
|
$detail = $val !== null ? number_format($val, 2, '.', '') . ($sym !== '' ? ' ' . $sym : '') : '';
|
||||||
|
if ($an !== '') {
|
||||||
|
$detail = trim($detail . ' · ' . $an, ' ·');
|
||||||
|
}
|
||||||
|
$events[] = [
|
||||||
|
'type' => 'expense',
|
||||||
|
'date' => $d,
|
||||||
|
'title' => $t,
|
||||||
|
'detail' => $detail,
|
||||||
|
'hrefQuery' => 'tab=currency',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$billDefs = normalizeCalendarBillDaysRaw($familySettings['calendar_bill_days'] ?? []);
|
||||||
|
foreach ($billDefs as $bd) {
|
||||||
|
$title = trim((string) ($bd['title'] ?? ''));
|
||||||
|
if ($title === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$single = [$bd];
|
||||||
|
foreach (calendarBillReminderDatesInRange($rangeStart, $rangeEnd, $single, $tzLocal) as $ymd) {
|
||||||
|
$events[] = [
|
||||||
|
'type' => 'bill_day',
|
||||||
|
'date' => $ymd,
|
||||||
|
'title' => $title,
|
||||||
|
'detail' => 'Bill reminder',
|
||||||
|
'hrefQuery' => 'tab=currency',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
usort($events, 'hubCalendarEventSortCompare');
|
||||||
|
return $events;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default two-week window from today in family timezone.
|
||||||
|
*
|
||||||
|
* @return array{0: string, 1: string} rangeStart, rangeEnd
|
||||||
|
*/
|
||||||
|
function hubCalendarDefaultAgendaRange(array $familySettings): array {
|
||||||
|
[, $tzLocal] = familyHubCalendarContext($familySettings);
|
||||||
|
$today = familyHubTodayYmdInTz($tzLocal);
|
||||||
|
try {
|
||||||
|
$end = (new DateTimeImmutable($today . ' 00:00:00', $tzLocal))->modify('+13 days')->format('Y-m-d');
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$end = $today;
|
||||||
|
}
|
||||||
|
return [$today, $end];
|
||||||
|
}
|
||||||
@ -2,22 +2,98 @@
|
|||||||
|
|
||||||
require_once __DIR__ . '/db.php';
|
require_once __DIR__ . '/db.php';
|
||||||
|
|
||||||
function defaultFamilySettings(): array {
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
function familyHubUsTimezoneIdentifiers(): array {
|
||||||
|
$ids = DateTimeZone::listIdentifiers(DateTimeZone::PER_COUNTRY, 'US');
|
||||||
|
if (is_array($ids) && $ids !== []) {
|
||||||
|
sort($ids);
|
||||||
|
return array_values($ids);
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
'Pacific/Honolulu',
|
||||||
|
'America/Anchorage',
|
||||||
|
'America/Los_Angeles',
|
||||||
|
'America/Phoenix',
|
||||||
|
'America/Denver',
|
||||||
|
'America/Chicago',
|
||||||
|
'America/New_York',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
function familySettingsDefaultsRaw(): array {
|
||||||
return [
|
return [
|
||||||
'currency_symbol' => '★',
|
'currency_symbol' => '★',
|
||||||
'currency_name' => 'Stars',
|
'currency_name' => 'Stars',
|
||||||
'currency_permanence' => 'permanent',
|
'currency_permanence' => 'permanent',
|
||||||
'timezone' => 'UTC',
|
'timezone' => 'America/New_York',
|
||||||
'week_starts_on' => 0,
|
'week_starts_on' => 0,
|
||||||
|
'calendar_two_way_google' => false,
|
||||||
|
'calendar_bill_days' => [],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $raw
|
||||||
|
* @return list<array{dayOfMonth: int, title: string}>
|
||||||
|
*/
|
||||||
|
function normalizeCalendarBillDaysRaw($raw): array {
|
||||||
|
if (!is_array($raw)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$out = [];
|
||||||
|
foreach ($raw as $row) {
|
||||||
|
if (!is_array($row)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$d = isset($row['dayOfMonth']) ? (int) $row['dayOfMonth'] : 0;
|
||||||
|
$title = trim((string) ($row['title'] ?? ''));
|
||||||
|
if ($d < 1 || $d > 31 || $title === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$out[] = ['dayOfMonth' => $d, 'title' => $title];
|
||||||
|
}
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTimezoneToUs(string $tz, array $allowed): string {
|
||||||
|
$tz = trim($tz);
|
||||||
|
if ($tz !== '' && in_array($tz, $allowed, true)) {
|
||||||
|
return $tz;
|
||||||
|
}
|
||||||
|
return 'America/New_York';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply defaults and normalize timezone / calendar fields after merge with stored JSON.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $s
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
function normalizeLoadedFamilySettings(array $s): array {
|
||||||
|
$allowed = familyHubUsTimezoneIdentifiers();
|
||||||
|
$s['timezone'] = normalizeTimezoneToUs((string) ($s['timezone'] ?? ''), $allowed);
|
||||||
|
$v = $s['calendar_two_way_google'] ?? false;
|
||||||
|
$s['calendar_two_way_google'] = $v === true || $v === 1 || $v === '1' || $v === 'true';
|
||||||
|
$s['calendar_bill_days'] = normalizeCalendarBillDaysRaw($s['calendar_bill_days'] ?? []);
|
||||||
|
return $s;
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultFamilySettings(): array {
|
||||||
|
return normalizeLoadedFamilySettings(familySettingsDefaultsRaw());
|
||||||
|
}
|
||||||
|
|
||||||
function loadFamilySettings(): array {
|
function loadFamilySettings(): array {
|
||||||
$raw = readJsonFile('family_settings.json');
|
$raw = readJsonFile('family_settings.json');
|
||||||
if (!is_array($raw)) {
|
$merged = familySettingsDefaultsRaw();
|
||||||
return defaultFamilySettings();
|
if (is_array($raw)) {
|
||||||
|
$merged = array_merge($merged, $raw);
|
||||||
}
|
}
|
||||||
return array_merge(defaultFamilySettings(), $raw);
|
return normalizeLoadedFamilySettings($merged);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
18
readme.md
18
readme.md
@ -3,7 +3,7 @@
|
|||||||
A centralized family organization system with tabs for chores, groceries, meal planning, family currency, and settings.
|
A centralized family organization system with tabs for chores, groceries, meal planning, family currency, and settings.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- Tabbed interface for chores, groceries, meals, shared Google Calendar embed, currency, and family settings (profiles, stores, preferences)
|
- Tabbed interface for chores, groceries, meals, combined Calendar (Family Hub agenda plus optional Google embed), currency, and family settings (profiles, stores, preferences)
|
||||||
- JSON-based data storage under `data/`
|
- JSON-based data storage under `data/`
|
||||||
- Daily automated exports (`scripts/daily_export.php` and `export.php?type=…`)
|
- Daily automated exports (`scripts/daily_export.php` and `export.php?type=…`)
|
||||||
- Mobile-friendly interface
|
- Mobile-friendly interface
|
||||||
@ -72,9 +72,21 @@ The **Calendar** tab embeds your family calendar in the hub using these values (
|
|||||||
| **`EXPORT_FREQUENCY`** | How often exports are expected to run | Your choice (e.g. `daily`). Match how you schedule **`scripts/daily_export.php`** in cron. |
|
| **`EXPORT_FREQUENCY`** | How often exports are expected to run | Your choice (e.g. `daily`). Match how you schedule **`scripts/daily_export.php`** in cron. |
|
||||||
| **`EXPORT_RETENTION_DAYS`** | How long to keep export files (interpreted by app logic) | Integer number of days (e.g. `30`). |
|
| **`EXPORT_RETENTION_DAYS`** | How long to keep export files (interpreted by app logic) | Integer number of days (e.g. `30`). |
|
||||||
|
|
||||||
|
## Calendar preferences (family settings)
|
||||||
|
|
||||||
|
These are **not** in `.env`; they live in `data/family_settings.json` and are edited under **Family settings → Calendar and dates**.
|
||||||
|
|
||||||
|
| Key / UI | Purpose | Notes |
|
||||||
|
| -------- | -------- | ----- |
|
||||||
|
| **Family timezone** | US IANA zone (e.g. `America/Denver`) | Dropdown of US zones only. Drives **today** and date ranges on the **Calendar** tab agenda. Options are the standard PHP US timezone identifiers (e.g. **America/***, **Pacific/Honolulu**). |
|
||||||
|
| **Enable two-way Google Calendar sync** | Opt-in for future OAuth sync with Google | Default off. When off, the Calendar tab uses the Google **embed** from `.env` (if set) and the local agenda only—no merge API. When on, the app may show sync messaging; full OAuth is not required for the embed. |
|
||||||
|
| **Monthly bill reminders** | `calendar_bill_days` | JSON array of `{ "dayOfMonth": 1–31, "title": "Rent" }`. Shown on the agenda on that calendar day each month. |
|
||||||
|
|
||||||
|
**API:** `GET api/calendar_events.php?from=YYYY-MM-DD&to=YYYY-MM-DD` returns `{ success, events, rangeStart, rangeEnd, today }` for the signed-in profile (same session rules as other APIs). Omit `from`/`to` to use the default two-week window from **today** in the family timezone.
|
||||||
|
|
||||||
## Directory Structure
|
## Directory Structure
|
||||||
familyHub/
|
familyHub/
|
||||||
├── api/ # JSON POST endpoints (chores, groceries, meals, people, settings, expenses, …)
|
├── api/ # JSON POST endpoints (chores, groceries, meals, people, settings, expenses, calendar_events, …)
|
||||||
├── assets/ # Static assets
|
├── assets/ # Static assets
|
||||||
│ ├── css/ # CSS files
|
│ ├── css/ # CSS files
|
||||||
│ │ └── style.css
|
│ │ └── style.css
|
||||||
@ -87,7 +99,7 @@ familyHub/
|
|||||||
├── data/ # JSON data storage (not tracked in git)
|
├── data/ # JSON data storage (not tracked in git)
|
||||||
│ ├── chores.json # Chores, assignments, submissions, reviews
|
│ ├── chores.json # Chores, assignments, submissions, reviews
|
||||||
│ ├── people.json # Profiles, roles, PIN hashes, currency balances
|
│ ├── people.json # Profiles, roles, PIN hashes, currency balances
|
||||||
│ ├── family_settings.json # Currency labels, timezone, week start, etc.
|
│ ├── family_settings.json # Currency labels, US timezone, week start, calendar merge flag, bill reminders, etc.
|
||||||
│ ├── expenses.json # Head-of-household expense log
|
│ ├── expenses.json # Head-of-household expense log
|
||||||
│ ├── stores.json # Grocery store definitions
|
│ ├── stores.json # Grocery store definitions
|
||||||
│ ├── grocery_lists.json # Per-store shopping lines
|
│ ├── grocery_lists.json # Per-store shopping lines
|
||||||
|
|||||||
@ -1,4 +1,8 @@
|
|||||||
<?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) : '';
|
$rawEmbed = defined('GOOGLE_CALENDAR_EMBED_CODE') ? trim((string) GOOGLE_CALENDAR_EMBED_CODE) : '';
|
||||||
$calId = defined('GOOGLE_CALENDAR_ID') ? trim((string) GOOGLE_CALENDAR_ID) : '';
|
$calId = defined('GOOGLE_CALENDAR_ID') ? trim((string) GOOGLE_CALENDAR_ID) : '';
|
||||||
|
|
||||||
@ -43,10 +47,90 @@ if ($embedUrl === null && $calId !== '' && $calId !== 'your_calendar_id_here') {
|
|||||||
$embedUrl = 'https://calendar.google.com/calendar/embed?src=' . rawurlencode($calId);
|
$embedUrl = 'https://calendar.google.com/calendar/embed?src=' . rawurlencode($calId);
|
||||||
$fromIdOnly = true;
|
$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">
|
<div id="calendar" class="tab-content">
|
||||||
<h2 class="mb-2">Calendar</h2>
|
<h2 class="mb-2">Calendar</h2>
|
||||||
|
|
||||||
|
<?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): ?>
|
<?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><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>
|
<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 else: ?>
|
<?php else: ?>
|
||||||
@ -56,7 +140,7 @@ if ($embedUrl === null && $calId !== '' && $calId !== 'your_calendar_id_here') {
|
|||||||
<div class="calendar-embed rounded border overflow-hidden bg-white">
|
<div class="calendar-embed rounded border overflow-hidden bg-white">
|
||||||
<iframe
|
<iframe
|
||||||
class="w-100 border-0 d-block"
|
class="w-100 border-0 d-block"
|
||||||
style="height: 70vh; min-height: 560px;"
|
style="height: 70vh; min-height: 360px;"
|
||||||
src="<?= htmlspecialchars($embedUrl, ENT_QUOTES, 'UTF-8') ?>"
|
src="<?= htmlspecialchars($embedUrl, ENT_QUOTES, 'UTF-8') ?>"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
title="Family calendar"
|
title="Family calendar"
|
||||||
@ -64,4 +148,6 @@ if ($embedUrl === null && $calId !== '' && $calId !== 'your_calendar_id_here') {
|
|||||||
></iframe>
|
></iframe>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -3,6 +3,13 @@ require_once __DIR__ . '/../includes/db.php';
|
|||||||
require_once __DIR__ . '/../includes/utils.php';
|
require_once __DIR__ . '/../includes/utils.php';
|
||||||
require_once __DIR__ . '/../includes/persona.php';
|
require_once __DIR__ . '/../includes/persona.php';
|
||||||
require_once __DIR__ . '/../includes/grocery_helpers.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
|
$canManage = count($people) === 0
|
||||||
|| (
|
|| (
|
||||||
@ -54,6 +61,73 @@ $permanenceOptions = [
|
|||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?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 mb-4">
|
||||||
<div class="card-header">Chore economy</div>
|
<div class="card-header">Chore economy</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@ -83,12 +157,6 @@ $permanenceOptions = [
|
|||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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">
|
<div class="col-md-6">
|
||||||
<label class="form-label" for="week_starts_on">Week starts on (0 = Sunday)</label>
|
<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"
|
<input type="number" class="form-control" id="week_starts_on" name="week_starts_on" min="0" max="6"
|
||||||
@ -97,7 +165,7 @@ $permanenceOptions = [
|
|||||||
</div>
|
</div>
|
||||||
<?php if ($canManage): ?>
|
<?php if ($canManage): ?>
|
||||||
<div class="col-12">
|
<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>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user