@@ -20,6 +20,14 @@ require __DIR__ . '/header.php';
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="ios-agenda-intro">Les événements viennent de HouseHub après synchro iCloud. <strong>Synchroniser</strong> met à jour tous les calendriers du compte ; les changements locaux partent au prochain envoi.</p>
|
<p class="ios-agenda-intro">Les événements viennent de HouseHub après synchro iCloud. <strong>Synchroniser</strong> met à jour tous les calendriers du compte ; les changements locaux partent au prochain envoi.</p>
|
||||||
|
|
||||||
|
<details class="ios-cal-prefs-details" id="ios-cal-prefs-details">
|
||||||
|
<summary class="ios-cal-prefs-summary">Calendriers affichés et couleurs</summary>
|
||||||
|
<p class="ios-cal-prefs-help">Décoche un calendrier pour le masquer dans l’agenda. La couleur est libre (repère la teinte iOS sur ton téléphone puis copie-la avec le sélecteur).</p>
|
||||||
|
<div id="ios-calendar-prefs-rows" class="ios-cal-prefs-rows"></div>
|
||||||
|
<p id="ios-calendar-prefs-msg" class="ios-cal-prefs-msg" role="status"></p>
|
||||||
|
<button type="button" class="pf-btn" id="ios-calendar-prefs-save">Enregistrer affichage & couleurs</button>
|
||||||
|
</details>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="pf-panel-card ios-agenda-card">
|
<section class="pf-panel-card ios-agenda-card">
|
||||||
|
|||||||
@@ -28,6 +28,12 @@ CREATE TABLE IF NOT EXISTS user_calendar_integrations (
|
|||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
UNIQUE KEY uq_user_provider (user_id, provider)
|
UNIQUE KEY uq_user_provider (user_id, provider)
|
||||||
)");
|
)");
|
||||||
|
try {
|
||||||
|
$meta_pdo->exec('ALTER TABLE user_calendar_integrations ADD COLUMN calendar_prefs_json TEXT NULL');
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
// colonne déjà présente
|
||||||
|
}
|
||||||
|
|
||||||
$pdo->exec("
|
$pdo->exec("
|
||||||
CREATE TABLE IF NOT EXISTS pf_calendar_events (
|
CREATE TABLE IF NOT EXISTS pf_calendar_events (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
@@ -90,11 +96,113 @@ function ios_normalize_datetime(?string $s): ?string
|
|||||||
return $s;
|
return $s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ios_calendar_url_key(?string $url): string
|
||||||
|
{
|
||||||
|
if ($url === null || $url === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return rtrim(trim($url), '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, array{visible:bool, color:?string}> */
|
||||||
|
function ios_calendar_prefs_decode(?string $json): array
|
||||||
|
{
|
||||||
|
if ($json === null || $json === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$d = json_decode($json, true);
|
||||||
|
if (!is_array($d)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$out = [];
|
||||||
|
foreach ($d as $k => $v) {
|
||||||
|
if (!is_string($k) || $k === '' || !is_array($v)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$key = ios_calendar_url_key($k);
|
||||||
|
if ($key === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$out[$key] = [
|
||||||
|
'visible' => !array_key_exists('visible', $v) ? true : (bool) $v['visible'],
|
||||||
|
'color' => (isset($v['color']) && is_string($v['color']) && preg_match('/^#[0-9A-Fa-f]{6}$/', $v['color'])) ? $v['color'] : null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array{url:string,url_key:string,display:string,visible:bool,color:?string}>
|
||||||
|
*/
|
||||||
|
function ios_list_calendar_sources_for_ui(array $integration, string $password, PDO $pdo): array
|
||||||
|
{
|
||||||
|
$prefs = ios_calendar_prefs_decode($integration['calendar_prefs_json'] ?? null);
|
||||||
|
$urlsMeta = [];
|
||||||
|
$primary = $integration['calendar_url'] ?? '';
|
||||||
|
if (hh_caldav_url_is_icloud($primary)) {
|
||||||
|
try {
|
||||||
|
foreach (hh_icloud_discover_calendar_entries($integration['username'], $password) as $e) {
|
||||||
|
$k = ios_calendar_url_key($e['url']);
|
||||||
|
if ($k === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$disp = trim((string) ($e['display'] ?? ''));
|
||||||
|
if ($disp === '') {
|
||||||
|
$disp = basename(parse_url($e['url'], PHP_URL_PATH) ?: '') ?: $k;
|
||||||
|
}
|
||||||
|
$urlsMeta[$k] = ['url' => rtrim($e['url'], '/') . '/', 'display' => $disp];
|
||||||
|
}
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$k = ios_calendar_url_key($primary);
|
||||||
|
if ($k !== '') {
|
||||||
|
$urlsMeta[$k] = ['url' => rtrim($primary, '/') . '/', 'display' => 'Calendrier'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$stmt = $pdo->query("
|
||||||
|
SELECT DISTINCT l.calendar_url AS u
|
||||||
|
FROM pf_calendar_event_links l
|
||||||
|
INNER JOIN pf_calendar_events e ON e.id = l.calendar_event_id
|
||||||
|
WHERE e.deleted_at IS NULL AND l.calendar_url IS NOT NULL AND TRIM(l.calendar_url) != ''
|
||||||
|
");
|
||||||
|
foreach ($stmt->fetchAll(PDO::FETCH_COLUMN) as $u) {
|
||||||
|
$k = ios_calendar_url_key((string) $u);
|
||||||
|
if ($k === '' || isset($urlsMeta[$k])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$urlsMeta[$k] = [
|
||||||
|
'url' => rtrim((string) $u, '/') . '/',
|
||||||
|
'display' => basename(parse_url((string) $u, PHP_URL_PATH) ?: '') ?: $k,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$out = [];
|
||||||
|
foreach ($urlsMeta as $key => $meta) {
|
||||||
|
$p = $prefs[$key] ?? [];
|
||||||
|
$visible = !array_key_exists('visible', $p) ? true : (bool) $p['visible'];
|
||||||
|
$color = (isset($p['color']) && is_string($p['color']) && preg_match('/^#[0-9A-Fa-f]{6}$/', $p['color'])) ? $p['color'] : null;
|
||||||
|
$out[] = [
|
||||||
|
'url' => $meta['url'],
|
||||||
|
'url_key' => $key,
|
||||||
|
'display' => $meta['display'],
|
||||||
|
'visible' => $visible,
|
||||||
|
'color' => $color,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
usort($out, static fn ($a, $b) => strcasecmp($a['display'], $b['display']));
|
||||||
|
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
$integrationStmt = $meta_pdo->prepare("SELECT * FROM user_calendar_integrations WHERE user_id = ? AND provider='icloud_caldav'");
|
$integrationStmt = $meta_pdo->prepare("SELECT * FROM user_calendar_integrations WHERE user_id = ? AND provider='icloud_caldav'");
|
||||||
$integrationStmt->execute([$userId]);
|
$integrationStmt->execute([$userId]);
|
||||||
$integration = $integrationStmt->fetch();
|
$integration = $integrationStmt->fetch();
|
||||||
|
|
||||||
if ($action === 'events' && $method === 'GET') {
|
if ($action === 'events' && $method === 'GET') {
|
||||||
|
$prefs = [];
|
||||||
|
if ($integration) {
|
||||||
|
$prefs = ios_calendar_prefs_decode($integration['calendar_prefs_json'] ?? null);
|
||||||
|
}
|
||||||
$rows = $pdo->query("
|
$rows = $pdo->query("
|
||||||
SELECT e.*, l.calendar_url AS calendar_source_url
|
SELECT e.*, l.calendar_url AS calendar_source_url
|
||||||
FROM pf_calendar_events e
|
FROM pf_calendar_events e
|
||||||
@@ -102,7 +210,73 @@ if ($action === 'events' && $method === 'GET') {
|
|||||||
WHERE e.deleted_at IS NULL
|
WHERE e.deleted_at IS NULL
|
||||||
ORDER BY e.start_at ASC
|
ORDER BY e.start_at ASC
|
||||||
")->fetchAll();
|
")->fetchAll();
|
||||||
ios_ok($rows);
|
$out = [];
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$key = ios_calendar_url_key($r['calendar_source_url'] ?? '');
|
||||||
|
if ($key !== '' && isset($prefs[$key]['visible']) && $prefs[$key]['visible'] === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$r['display_color'] = null;
|
||||||
|
if ($key !== '' && !empty($prefs[$key]['color']) && is_string($prefs[$key]['color']) && preg_match('/^#[0-9A-Fa-f]{6}$/', $prefs[$key]['color'])) {
|
||||||
|
$r['display_color'] = $prefs[$key]['color'];
|
||||||
|
}
|
||||||
|
$out[] = $r;
|
||||||
|
}
|
||||||
|
ios_ok($out);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'calendar_sources' && $method === 'GET') {
|
||||||
|
if (!$integration) {
|
||||||
|
ios_ok(['calendars' => []]);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$ctx = ios_caldav_prepare($integration, $meta_pdo);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
ios_err('CalDAV: ' . $e->getMessage(), 400);
|
||||||
|
}
|
||||||
|
$integrationStmt->execute([$userId]);
|
||||||
|
$integrationFresh = $integrationStmt->fetch();
|
||||||
|
if (!$integrationFresh) {
|
||||||
|
ios_ok(['calendars' => []]);
|
||||||
|
}
|
||||||
|
ios_ok(['calendars' => ios_list_calendar_sources_for_ui($integrationFresh, $ctx['password'], $pdo)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'calendar_prefs' && $method === 'POST') {
|
||||||
|
ios_require_csrf();
|
||||||
|
if (!$integration) {
|
||||||
|
ios_err('Connexion iCloud non configurée.', 400);
|
||||||
|
}
|
||||||
|
$body = ios_body();
|
||||||
|
$raw = $body['prefs'] ?? null;
|
||||||
|
if (!is_array($raw)) {
|
||||||
|
ios_err('Format prefs invalide', 400);
|
||||||
|
}
|
||||||
|
$clean = [];
|
||||||
|
foreach ($raw as $key => $p) {
|
||||||
|
if (!is_array($p)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$urlKey = ios_calendar_url_key(is_string($key) ? $key : '');
|
||||||
|
if ($urlKey === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$visible = array_key_exists('visible', $p) ? (bool) $p['visible'] : true;
|
||||||
|
$col = $p['color'] ?? null;
|
||||||
|
if ($col !== null && $col !== '' && is_string($col) && preg_match('/^#[0-9A-Fa-f]{6}$/', $col)) {
|
||||||
|
$col = '#' . strtolower(substr($col, 1));
|
||||||
|
} else {
|
||||||
|
$col = null;
|
||||||
|
}
|
||||||
|
$clean[$urlKey] = ['visible' => $visible, 'color' => $col];
|
||||||
|
}
|
||||||
|
$existing = ios_calendar_prefs_decode($integration['calendar_prefs_json'] ?? null);
|
||||||
|
foreach ($clean as $urlKey => $v) {
|
||||||
|
$existing[$urlKey] = $v;
|
||||||
|
}
|
||||||
|
$json = json_encode($existing, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
|
$meta_pdo->prepare('UPDATE user_calendar_integrations SET calendar_prefs_json = ? WHERE id = ?')->execute([$json, $integration['id']]);
|
||||||
|
ios_ok(['saved' => true]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($action === 'events' && $method === 'POST') {
|
if ($action === 'events' && $method === 'POST') {
|
||||||
|
|||||||
@@ -24,6 +24,102 @@
|
|||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ios-cal-prefs-details {
|
||||||
|
margin-top: 14px;
|
||||||
|
border: 1px solid var(--pf-border, #e2e8f0);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
background: var(--pf-bg-page, #fff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ios-cal-prefs-summary {
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ios-cal-prefs-help {
|
||||||
|
margin: 10px 0 12px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--pf-text-muted, #64748b);
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ios-cal-prefs-rows {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ios-cal-pref-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr auto auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px 12px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid var(--pf-border, #e2e8f0);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--pf-bg-lighter, #f8fafc);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ios-cal-pref-row input[type="checkbox"] {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ios-cal-pref-label {
|
||||||
|
font-size: 0.88rem;
|
||||||
|
min-width: 0;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ios-cal-pref-color {
|
||||||
|
width: 44px;
|
||||||
|
height: 36px;
|
||||||
|
padding: 2px;
|
||||||
|
border: 1px solid var(--pf-border, #e2e8f0);
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: var(--pf-bg-page, #fff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ios-cal-pref-reset {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--pf-text-muted, #64748b);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px;
|
||||||
|
font-family: inherit;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ios-cal-pref-reset:hover {
|
||||||
|
color: var(--pf-text-main, #0f172a);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ios-cal-prefs-msg {
|
||||||
|
min-height: 1.25em;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin: 0 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.ios-cal-pref-row {
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
grid-template-rows: auto auto;
|
||||||
|
}
|
||||||
|
.ios-cal-pref-color {
|
||||||
|
grid-column: 2;
|
||||||
|
}
|
||||||
|
.ios-cal-pref-reset {
|
||||||
|
grid-column: 2;
|
||||||
|
justify-self: start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.ios-form-grid {
|
.ios-form-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
@@ -349,8 +445,10 @@
|
|||||||
box-shadow: inset 3px 0 0 rgba(255, 255, 255, 0.35);
|
box-shadow: inset 3px 0 0 rgba(255, 255, 255, 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ios-event-block:hover {
|
.ios-event-block--custom {
|
||||||
filter: brightness(1.08);
|
border: none !important;
|
||||||
|
color: #fff !important;
|
||||||
|
box-shadow: inset 3px 0 0 rgba(255, 255, 255, 0.35) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ios-event-block-title {
|
.ios-event-block-title {
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ const PALETTE = [
|
|||||||
"ios-cal-c5",
|
"ios-cal-c5",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const SUGGEST_HEX = ["#22c55e", "#3b82f6", "#8b5cf6", "#eab308", "#14b8a6", "#f43f5e"];
|
||||||
|
|
||||||
function escHtml(s) {
|
function escHtml(s) {
|
||||||
const d = document.createElement("div");
|
const d = document.createElement("div");
|
||||||
d.textContent = s ?? "";
|
d.textContent = s ?? "";
|
||||||
@@ -50,6 +52,21 @@ function parseEvtDate(iso) {
|
|||||||
return Number.isNaN(d.getTime()) ? null : d;
|
return Number.isNaN(d.getTime()) ? null : d;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function darkenHex(hex, amount) {
|
||||||
|
const n = parseInt(hex.slice(1), 16);
|
||||||
|
const r = Math.max(0, (n >> 16) - amount);
|
||||||
|
const g = Math.max(0, ((n >> 8) & 0xff) - amount);
|
||||||
|
const b = Math.max(0, (n & 0xff) - amount);
|
||||||
|
return `#${[r, g, b].map((x) => x.toString(16).padStart(2, "0")).join("")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function suggestedColorForKey(key) {
|
||||||
|
const k = String(key || "");
|
||||||
|
let h = 0;
|
||||||
|
for (let i = 0; i < k.length; i++) h = (h * 31 + k.charCodeAt(i)) >>> 0;
|
||||||
|
return SUGGEST_HEX[h % SUGGEST_HEX.length];
|
||||||
|
}
|
||||||
|
|
||||||
function colorClassForEvent(evt) {
|
function colorClassForEvent(evt) {
|
||||||
const key = String(evt.calendar_source_url || evt.external_uid || evt.id || "");
|
const key = String(evt.calendar_source_url || evt.external_uid || evt.id || "");
|
||||||
let h = 0;
|
let h = 0;
|
||||||
@@ -57,6 +74,22 @@ function colorClassForEvent(evt) {
|
|||||||
return PALETTE[h % PALETTE.length];
|
return PALETTE[h % PALETTE.length];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Applique couleur perso (API) ou palette par défaut */
|
||||||
|
function styleEventBlock(el, evt) {
|
||||||
|
el.className = "ios-event-block";
|
||||||
|
el.style.background = "";
|
||||||
|
el.style.boxShadow = "";
|
||||||
|
const hex = evt.display_color;
|
||||||
|
if (hex && /^#[0-9A-Fa-f]{6}$/i.test(hex)) {
|
||||||
|
const c = hex.startsWith("#") ? hex : `#${hex.slice(-6)}`;
|
||||||
|
el.classList.add("ios-event-block--custom");
|
||||||
|
el.style.background = `linear-gradient(135deg, ${darkenHex(c, 40)} 0%, ${c} 100%)`;
|
||||||
|
el.style.boxShadow = "inset 3px 0 0 rgba(255,255,255,0.35)";
|
||||||
|
} else {
|
||||||
|
el.classList.add(colorClassForEvent(evt));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function mondayOf(date) {
|
function mondayOf(date) {
|
||||||
const d = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
const d = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
||||||
const dow = d.getDay();
|
const dow = d.getDay();
|
||||||
@@ -243,7 +276,7 @@ function buildDayColumn(dayMidnight) {
|
|||||||
const left = (100 * seg._lane) / seg._laneCount;
|
const left = (100 * seg._lane) / seg._laneCount;
|
||||||
const blk = document.createElement("button");
|
const blk = document.createElement("button");
|
||||||
blk.type = "button";
|
blk.type = "button";
|
||||||
blk.className = `ios-event-block ${colorClassForEvent(seg.evt)}`;
|
styleEventBlock(blk, seg.evt);
|
||||||
blk.style.top = `${top}px`;
|
blk.style.top = `${top}px`;
|
||||||
blk.style.height = `${h}px`;
|
blk.style.height = `${h}px`;
|
||||||
blk.style.left = `calc(${left}% + 2px)`;
|
blk.style.left = `calc(${left}% + 2px)`;
|
||||||
@@ -442,6 +475,9 @@ async function loadEvents() {
|
|||||||
events.forEach((evt) => {
|
events.forEach((evt) => {
|
||||||
const card = document.createElement("div");
|
const card = document.createElement("div");
|
||||||
card.className = "ios-event-card";
|
card.className = "ios-event-card";
|
||||||
|
if (evt.display_color && /^#[0-9A-Fa-f]{6}$/i.test(evt.display_color)) {
|
||||||
|
card.style.borderLeft = `4px solid ${evt.display_color}`;
|
||||||
|
}
|
||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
<div class="ios-event-card-head">
|
<div class="ios-event-card-head">
|
||||||
<div>
|
<div>
|
||||||
@@ -476,6 +512,94 @@ async function loadSyncStatus() {
|
|||||||
iosSyncStatus.textContent = status.message;
|
iosSyncStatus.textContent = status.message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadCalendarPrefs() {
|
||||||
|
const wrap = document.getElementById("ios-calendar-prefs-rows");
|
||||||
|
const msg = document.getElementById("ios-calendar-prefs-msg");
|
||||||
|
if (!wrap) return;
|
||||||
|
if (msg) msg.textContent = "";
|
||||||
|
wrap.innerHTML = "<span class=\"ios-agenda-intro\">Chargement des calendriers…</span>";
|
||||||
|
try {
|
||||||
|
const data = await iosApi("calendar_sources");
|
||||||
|
const rows = data.calendars || [];
|
||||||
|
wrap.innerHTML = "";
|
||||||
|
if (!rows.length) {
|
||||||
|
wrap.innerHTML =
|
||||||
|
"<span class=\"ios-agenda-intro\">Aucun calendrier détecté. Configure iCloud dans Paramètres puis synchronise.</span>";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rows.forEach((row, idx) => {
|
||||||
|
const id = `ios-cal-cb-${idx}`;
|
||||||
|
const rowEl = document.createElement("div");
|
||||||
|
rowEl.className = "ios-cal-pref-row";
|
||||||
|
rowEl.dataset.urlKey = row.url_key;
|
||||||
|
|
||||||
|
const cb = document.createElement("input");
|
||||||
|
cb.type = "checkbox";
|
||||||
|
cb.id = id;
|
||||||
|
cb.checked = row.visible !== false;
|
||||||
|
|
||||||
|
const lab = document.createElement("label");
|
||||||
|
lab.className = "ios-cal-pref-label";
|
||||||
|
lab.htmlFor = id;
|
||||||
|
lab.textContent = row.display || row.url_key;
|
||||||
|
|
||||||
|
const colInp = document.createElement("input");
|
||||||
|
colInp.type = "color";
|
||||||
|
colInp.className = "ios-cal-pref-color";
|
||||||
|
colInp.value = row.color || suggestedColorForKey(row.url_key);
|
||||||
|
colInp.title = "Couleur dans l’agenda";
|
||||||
|
|
||||||
|
const autoBtn = document.createElement("button");
|
||||||
|
autoBtn.type = "button";
|
||||||
|
autoBtn.className = "ios-cal-pref-reset";
|
||||||
|
autoBtn.textContent = "Palette auto";
|
||||||
|
autoBtn.title = "Couleurs HouseHub par défaut (sans teinte iOS)";
|
||||||
|
autoBtn.addEventListener("click", () => {
|
||||||
|
rowEl.dataset.paletteAuto = "1";
|
||||||
|
colInp.disabled = true;
|
||||||
|
colInp.style.opacity = "0.35";
|
||||||
|
});
|
||||||
|
|
||||||
|
rowEl.appendChild(cb);
|
||||||
|
rowEl.appendChild(lab);
|
||||||
|
rowEl.appendChild(colInp);
|
||||||
|
rowEl.appendChild(autoBtn);
|
||||||
|
wrap.appendChild(rowEl);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
wrap.innerHTML = `<span class="ios-agenda-intro">${escHtml(e.message)}</span>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveCalendarPrefs() {
|
||||||
|
const msg = document.getElementById("ios-calendar-prefs-msg");
|
||||||
|
const wrap = document.getElementById("ios-calendar-prefs-rows");
|
||||||
|
if (!wrap) return;
|
||||||
|
const prefs = {};
|
||||||
|
wrap.querySelectorAll(".ios-cal-pref-row").forEach((rowEl) => {
|
||||||
|
const key = rowEl.dataset.urlKey;
|
||||||
|
if (!key) return;
|
||||||
|
const vis = rowEl.querySelector('input[type="checkbox"]')?.checked === true;
|
||||||
|
const colInp = rowEl.querySelector(".ios-cal-pref-color");
|
||||||
|
let color = null;
|
||||||
|
if (!rowEl.dataset.paletteAuto && colInp && !colInp.disabled) {
|
||||||
|
const v = colInp.value;
|
||||||
|
if (v && /^#[0-9A-Fa-f]{6}$/i.test(v)) {
|
||||||
|
color = v.startsWith("#") ? v.toLowerCase() : `#${v}`.toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prefs[key] = { visible: vis, color };
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await iosApi("calendar_prefs", { method: "POST", body: { prefs } });
|
||||||
|
if (msg) msg.textContent = "Préférences enregistrées.";
|
||||||
|
await loadEvents();
|
||||||
|
await loadCalendarPrefs();
|
||||||
|
} catch (e) {
|
||||||
|
if (msg) msg.textContent = e.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
iosForm.addEventListener("submit", async (e) => {
|
iosForm.addEventListener("submit", async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const idStr = document.getElementById("ios-event-id").value.trim();
|
const idStr = document.getElementById("ios-event-id").value.trim();
|
||||||
@@ -505,6 +629,7 @@ document.getElementById("ios-sync-btn")?.addEventListener("click", async () => {
|
|||||||
const data = await iosApi("sync", { method: "POST", body: {} });
|
const data = await iosApi("sync", { method: "POST", body: {} });
|
||||||
if (iosSyncStatus) iosSyncStatus.textContent = data.message;
|
if (iosSyncStatus) iosSyncStatus.textContent = data.message;
|
||||||
await loadEvents();
|
await loadEvents();
|
||||||
|
await loadCalendarPrefs();
|
||||||
} finally {
|
} finally {
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
}
|
}
|
||||||
@@ -558,10 +683,13 @@ document.getElementById("ios-agenda-add")?.addEventListener("click", () => {
|
|||||||
document.getElementById("ios-title")?.focus();
|
document.getElementById("ios-title")?.focus();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.getElementById("ios-calendar-prefs-save")?.addEventListener("click", () => saveCalendarPrefs());
|
||||||
|
|
||||||
window.addEventListener("DOMContentLoaded", async () => {
|
window.addEventListener("DOMContentLoaded", async () => {
|
||||||
try {
|
try {
|
||||||
iosCursor = new Date();
|
iosCursor = new Date();
|
||||||
await loadEvents();
|
await loadEvents();
|
||||||
|
await loadCalendarPrefs();
|
||||||
await loadSyncStatus();
|
await loadSyncStatus();
|
||||||
showListSection(false);
|
showListSection(false);
|
||||||
startNowTimer();
|
startNowTimer();
|
||||||
|
|||||||
Reference in New Issue
Block a user