Premiere commit - Omika

This commit is contained in:
2026-10-23 18:46:50 +02:00
commit 8ea76c1b24
2155 changed files with 984301 additions and 0 deletions
+622
View File
@@ -0,0 +1,622 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="theme-color" content="#4f46e5" id="themeMeta">
<meta name="color-scheme" content="light dark">
<title>Omika — Maquette fonctionnelle terrain</title>
<style>
/* =========================================================
OMIKA — DESIGN SYSTEM, THÈMES ET MISE EN PAGE RESPONSIVE
========================================================= */
:root{
--primary:#4f46e5;--primary-dark:#3730a3;--primary-soft:#eeedff;--on-primary:#fff;
--bg:#f3f6fb;--surface:#fff;--surface-2:#f8fafc;--surface-3:#eef2f7;
--text:#172033;--muted:#617086;--faint:#8a96a8;--border:#dfe6ef;
--success:#14845b;--success-soft:#e7f7f0;--warning:#ae6800;--warning-soft:#fff3d8;
--danger:#c3414c;--danger-soft:#ffeaec;--info:#1e73be;--info-soft:#e8f3ff;
--shadow:0 10px 32px rgba(24,35,58,.09);--radius:17px;--rail:250px;--top:68px;
font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
}
body[data-theme="dark"]{
--bg:#0c1320;--surface:#141d2b;--surface-2:#192435;--surface-3:#202d40;
--text:#f4f7fb;--muted:#bec8d7;--faint:#8e9aaf;--border:#2b394c;
--success-soft:rgba(20,132,91,.2);--warning-soft:rgba(174,104,0,.23);
--danger-soft:rgba(195,65,76,.2);--info-soft:rgba(30,115,190,.22);
--shadow:0 14px 40px rgba(0,0,0,.3)
}
*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;min-width:320px;background:var(--bg);color:var(--text)}
button,input,select,textarea{font:inherit;color:inherit}button{cursor:pointer}a{color:inherit}.hidden{display:none!important}
.muted{color:var(--muted)}.faint{color:var(--faint)}.small{font-size:12px}.tiny{font-size:10px}.mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-variant-numeric:tabular-nums}
.ico{width:1.35em;height:1.35em;display:inline-grid;place-items:center;flex:0 0 auto;font-style:normal;line-height:1}
.app{min-height:100vh}.rail{position:fixed;inset:0 auto 0 0;width:var(--rail);z-index:30;display:flex;flex-direction:column;padding:16px 12px;background:var(--surface);border-right:1px solid var(--border)}
.brand{display:flex;align-items:center;gap:11px;padding:2px 8px 18px}.brand-mark{width:43px;height:43px;display:grid;place-items:center;border-radius:14px;background:linear-gradient(145deg,var(--primary),var(--primary-dark));color:var(--on-primary);font-weight:950;letter-spacing:-.05em;box-shadow:0 8px 18px color-mix(in srgb,var(--primary) 28%,transparent)}
.brand h1{margin:0;font-size:19px;letter-spacing:-.04em}.brand p{margin:2px 0 0;color:var(--faint);font-size:10px}.nav-label{margin:8px 10px;color:var(--faint);font-size:10px;font-weight:850;letter-spacing:.1em;text-transform:uppercase}
.nav{display:grid;gap:5px}.nav button,.mobile-nav button{border:0;background:transparent}.nav button{min-height:46px;display:flex;align-items:center;gap:12px;padding:10px 12px;border-radius:12px;color:var(--muted);text-align:left}.nav button:hover{background:var(--surface-2);color:var(--text)}.nav button.active{background:var(--primary-soft);color:var(--primary);font-weight:850}.nav .count{margin-left:auto;min-width:23px;height:23px;display:grid;place-items:center;padding:0 6px;border-radius:999px;background:var(--primary);color:var(--on-primary);font-size:10px}
.rail-spacer{flex:1}.rail-sync{margin:12px 3px;padding:11px;border:1px solid var(--border);border-radius:13px;background:var(--surface-2);font-size:11px;color:var(--muted)}.profile{display:flex;align-items:center;gap:9px;padding:8px}.avatar{width:38px;height:38px;display:grid;place-items:center;flex:0 0 auto;border-radius:50%;color:#fff;background:#7657c8;font-size:11px;font-weight:900}.profile strong,.profile span{display:block}.profile strong{font-size:12px}.profile span{color:var(--faint);font-size:10px;margin-top:2px}
.shell{min-height:100vh;margin-left:var(--rail)}.topbar{position:sticky;top:0;z-index:25;height:var(--top);display:flex;align-items:center;justify-content:space-between;gap:12px;padding:0 clamp(16px,3vw,38px);border-bottom:1px solid var(--border);background:color-mix(in srgb,var(--bg) 88%,transparent);backdrop-filter:blur(16px)}.topbar-left,.topbar-actions{display:flex;align-items:center;gap:9px;min-width:0}.topbar h2{margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:18px;letter-spacing:-.02em}.topbar small{display:block;color:var(--faint);font-size:10px;margin-bottom:2px}.menu-btn{display:none}
main{width:min(100%,1440px);margin:0 auto;padding:clamp(18px,3vw,34px) clamp(14px,3vw,38px) 58px}.mobile-nav{display:none}.screen{animation:screenIn .18s ease both}@keyframes screenIn{from{opacity:0;transform:translateY(3px)}to{opacity:1;transform:none}}
.page-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:20px}.page-head h1{margin:0;font-size:clamp(25px,3vw,35px);letter-spacing:-.045em}.page-head p{margin:7px 0 0;max-width:780px;color:var(--muted);line-height:1.55}.actions{display:flex;gap:8px;flex-wrap:wrap}.stack{display:grid;gap:12px}.stack-lg{display:grid;gap:18px}.cluster{display:flex;align-items:center;justify-content:space-between;gap:12px}.inline{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.grid-2{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:13px}.grid-3{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:13px}.grid-4{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}.span-2{grid-column:span 2}
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);box-shadow:var(--shadow)}.card-head{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:17px 19px;border-bottom:1px solid var(--border)}.card-head h2,.card-head h3{margin:0;font-size:16px}.card-head p{margin:3px 0 0;color:var(--faint);font-size:11px}.card-body{padding:18px}
.btn,.icon-btn{border:1px solid transparent;border-radius:11px;font-weight:780;transition:.16s ease}.btn{min-height:42px;display:inline-flex;align-items:center;justify-content:center;gap:7px;padding:9px 14px;font-size:13px}.btn:hover,.icon-btn:hover{transform:translateY(-1px)}.btn:disabled,.icon-btn:disabled{opacity:.55;cursor:not-allowed;transform:none}.btn-primary{background:var(--primary);border-color:var(--primary);color:var(--on-primary);box-shadow:0 8px 18px color-mix(in srgb,var(--primary) 22%,transparent)}.btn-secondary{background:var(--surface);border-color:var(--border)}.btn-soft{background:var(--primary-soft);border-color:var(--primary-soft);color:var(--primary)}.btn-danger{background:var(--danger-soft);color:var(--danger)}.btn-ghost{background:transparent;color:var(--muted)}.btn-sm{min-height:34px;padding:6px 10px;font-size:11px}.btn-block{width:100%}.icon-btn{width:42px;height:42px;display:inline-grid;place-items:center;padding:0;background:var(--surface);border-color:var(--border);color:var(--muted)}button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible,canvas:focus-visible{outline:3px solid color-mix(in srgb,var(--primary) 28%,transparent);outline-offset:2px}
.badge,.status{display:inline-flex;align-items:center;gap:5px;min-height:24px;width:max-content;max-width:100%;padding:4px 9px;border-radius:999px;font-size:10px;font-weight:850}.badge{background:var(--surface-3);color:var(--muted)}.badge.primary{background:var(--primary-soft);color:var(--primary)}.badge.success{background:var(--success-soft);color:var(--success)}.badge.warning{background:var(--warning-soft);color:var(--warning)}.badge.danger{background:var(--danger-soft);color:var(--danger)}.badge.info{background:var(--info-soft);color:var(--info)}.status::before{content:"";width:7px;height:7px;border-radius:50%;background:currentColor}.planned{background:var(--info-soft);color:var(--info)}.progress{background:var(--warning-soft);color:var(--warning)}.completed{background:var(--success-soft);color:var(--success)}.late{background:var(--danger-soft);color:var(--danger)}
.field{display:grid;gap:6px}.field label,.field-title{font-size:12px;font-weight:820}.required::after{content:" *";color:var(--danger)}input,select,textarea{width:100%;min-height:43px;padding:10px 12px;border:1px solid var(--border);border-radius:10px;background:var(--surface);color:var(--text)}textarea{min-height:105px;resize:vertical;line-height:1.5}input::placeholder,textarea::placeholder{color:var(--faint)}.check{display:flex;align-items:flex-start;gap:8px;color:var(--muted);font-size:12px;line-height:1.45}.check input{width:18px;height:18px;min-height:auto;margin-top:1px;accent-color:var(--primary)}.form-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}.checks{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:9px}.segmented{display:inline-flex;gap:3px;padding:4px;border:1px solid var(--border);border-radius:12px;background:var(--surface-2)}.segmented button{min-height:33px;padding:6px 11px;border:0;border-radius:8px;background:transparent;color:var(--muted);font-size:11px;font-weight:820}.segmented button.active{background:var(--surface);color:var(--primary);box-shadow:0 2px 7px rgba(0,0,0,.07)}
.welcome{position:relative;overflow:hidden;display:grid;grid-template-columns:minmax(0,1.35fr) minmax(260px,.65fr);gap:16px;margin-bottom:18px;padding:23px;border:1px solid var(--border);border-radius:22px;background:linear-gradient(135deg,var(--surface),var(--primary-soft));box-shadow:var(--shadow)}.welcome:after{content:"";position:absolute;right:-80px;top:-120px;width:250px;height:250px;border-radius:50%;background:color-mix(in srgb,var(--primary) 9%,transparent)}.welcome>*{position:relative;z-index:1}.welcome h1{margin:9px 0 6px;font-size:clamp(25px,3vw,34px);letter-spacing:-.04em}.welcome p{margin:0 0 17px;color:var(--muted);line-height:1.5}.summary-box{align-self:center;padding:16px;border:1px solid var(--border);border-radius:15px;background:color-mix(in srgb,var(--surface) 72%,transparent)}.summary-box strong{display:block;font-size:30px}.progressbar{height:7px;margin-top:10px;overflow:hidden;border-radius:99px;background:var(--surface-3)}.progressbar span{display:block;height:100%;background:var(--primary)}
.stat{display:flex;align-items:center;gap:11px;min-height:88px;padding:14px}.stat-icon{width:44px;height:44px;display:grid;place-items:center;border-radius:13px;background:var(--primary-soft);color:var(--primary);font-size:19px}.stat strong{display:block;font-size:20px}.stat span{display:block;margin-top:2px;color:var(--muted);font-size:10px;line-height:1.35}
.agenda{overflow:hidden}.agenda-tools{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 16px;border-bottom:1px solid var(--border)}.tool-group{display:flex;align-items:center;gap:7px}.period{min-width:260px;text-align:center;font-size:13px;font-weight:850}.date-strip{display:grid;grid-template-columns:repeat(7,1fr);gap:5px;padding:10px 12px;border-bottom:1px solid var(--border);background:var(--surface-2)}.date-chip{min-width:0;padding:8px 3px;border:0;border-radius:11px;background:transparent;color:var(--muted)}.date-chip:hover{background:var(--surface)}.date-chip.selected{background:var(--primary);color:var(--on-primary);box-shadow:0 6px 14px color-mix(in srgb,var(--primary) 23%,transparent)}.date-chip:disabled{opacity:.35}.date-chip span,.date-chip strong,.date-chip small{display:block}.date-chip span{font-size:9px;text-transform:uppercase}.date-chip strong{font-size:17px;margin:2px 0}.date-chip small{font-size:8px}.list{display:grid}.day-group+.day-group{border-top:1px solid var(--border)}.day-title{display:flex;justify-content:space-between;gap:10px;padding:12px 16px;background:var(--surface-2);font-size:11px;font-weight:820}.appt{position:relative;display:grid;grid-template-columns:68px minmax(0,1fr) auto;gap:13px;align-items:center;padding:14px 16px;border-bottom:1px solid var(--border);background:var(--surface);transition:.16s}.appt:last-child{border-bottom:0}.appt:hover{background:var(--surface-2)}.appt:before{content:"";position:absolute;left:0;top:10px;bottom:10px;width:4px;border-radius:0 99px 99px 0;background:var(--info)}.appt.is-completed:before{background:var(--success)}.appt.is-progress:before{background:var(--warning)}.appt.is-late:before{background:var(--danger)}.appt-time strong,.appt-time span{display:block}.appt-time strong{font-size:17px}.appt-time span{margin-top:2px;color:var(--faint);font-size:10px}.appt h3{margin:7px 0 4px;font-size:14px}.appt p{margin:0;color:var(--muted);font-size:11px;line-height:1.4}.meta{display:flex;gap:9px;flex-wrap:wrap;margin-top:7px;color:var(--faint);font-size:9px}.appt-action{display:flex;align-items:center}.link-state{margin-top:6px}
.back{display:flex;align-items:center;gap:8px;margin-bottom:14px}.hero{overflow:hidden;padding:24px;margin-bottom:18px;border-radius:22px;background:linear-gradient(135deg,var(--primary-dark),var(--primary));color:var(--on-primary);box-shadow:0 14px 35px color-mix(in srgb,var(--primary) 24%,transparent)}.hero-top{display:flex;align-items:flex-start;justify-content:space-between;gap:14px}.hero h1{margin:10px 0 6px;font-size:clamp(24px,3vw,34px);letter-spacing:-.04em}.hero p{margin:0;color:color-mix(in srgb,var(--on-primary) 78%,transparent);line-height:1.5}.hero .status{background:color-mix(in srgb,var(--surface) 16%,transparent);color:var(--on-primary)}.hero-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:20px}.hero-actions .btn{background:color-mix(in srgb,var(--surface) 14%,transparent);border-color:color-mix(in srgb,var(--surface) 20%,transparent);color:var(--on-primary)}.hero-actions .btn.main{background:var(--surface);border-color:var(--surface);color:var(--primary-dark)}
.detail-layout{display:grid;grid-template-columns:minmax(0,1fr) 330px;gap:18px;align-items:start}.detail-side{position:sticky;top:calc(var(--top) + 18px);display:grid;gap:16px}.info{min-width:0;padding:13px;border:1px solid var(--border);border-radius:12px;background:var(--surface-2)}.info span{display:block;margin-bottom:5px;color:var(--faint);font-size:9px;text-transform:uppercase;letter-spacing:.05em}.info strong{display:block;font-size:12px;line-height:1.45;overflow-wrap:anywhere}.text-box{padding:14px;border-left:3px solid var(--primary);border-radius:0 11px 11px 0;background:var(--surface-2)}.text-box h4{margin:0 0 6px;font-size:12px}.text-box p{margin:0;color:var(--muted);font-size:12px;line-height:1.55}.map{position:relative;height:165px;overflow:hidden;background:linear-gradient(35deg,#d6e3d3,#e8eef5)}body[data-theme="dark"] .map{background:linear-gradient(35deg,#263528,#28394b)}.map:before,.map:after{content:"";position:absolute;inset:-40px;transform:rotate(16deg);border:14px solid rgba(255,255,255,.5);border-left:0;border-right:0}.map:after{transform:rotate(-32deg);border-width:8px}.pin{position:absolute;inset:0;display:grid;place-items:center}.pin span{width:48px;height:48px;display:grid;place-items:center;border:5px solid rgba(255,255,255,.8);border-radius:50% 50% 50% 8px;transform:rotate(-45deg);background:var(--primary);color:var(--on-primary);font-size:20px;box-shadow:0 10px 18px rgba(0,0,0,.2)}.pin span::first-letter{transform:rotate(45deg)}
.row{display:flex;align-items:center;gap:11px;min-width:0;padding:11px;border:1px solid var(--border);border-radius:12px;background:var(--surface-2)}.row-icon{width:38px;height:38px;display:grid;place-items:center;flex:0 0 auto;border-radius:11px;background:var(--primary-soft);color:var(--primary);font-weight:900}.row-main{min-width:0;flex:1}.row-main strong,.row-main span{display:block}.row-main strong{font-size:11px;overflow-wrap:anywhere}.row-main span{margin-top:4px;color:var(--muted);font-size:10px;line-height:1.45;overflow-wrap:anywhere}.row-actions{display:flex;gap:6px;flex-wrap:wrap}
.resource-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px}.resource-card{display:grid;gap:10px;padding:16px;border:1px solid var(--border);border-radius:14px;background:var(--surface-2);text-align:left}.resource-card:hover{border-color:var(--primary)}.resource-card .resource-icon{width:42px;height:42px;display:grid;place-items:center;border-radius:12px;background:var(--primary-soft);color:var(--primary);font-size:18px}.resource-card strong{font-size:13px}.resource-card span{color:var(--muted);font-size:10px;line-height:1.4}.resource-card b{color:var(--primary);font-size:11px}
.callout{display:flex;align-items:flex-start;gap:11px;padding:14px;border:1px solid var(--border);border-radius:14px;background:var(--primary-soft)}.callout .row-icon{background:var(--surface)}.callout h3{margin:0 0 4px;font-size:13px}.callout p{margin:0;color:var(--muted);font-size:11px;line-height:1.5}.callout .actions{margin-left:auto;flex:0 0 auto}
.time-summary{display:grid;grid-template-columns:repeat(3,1fr);gap:9px}.time-stat{padding:12px;border:1px solid var(--border);border-radius:11px;background:var(--surface-2)}.time-stat strong{display:block;font-size:17px}.time-stat span{display:block;margin-top:3px;color:var(--muted);font-size:9px}.time-entry{display:grid;grid-template-columns:minmax(0,1fr) auto auto auto;gap:10px;align-items:center}.time-entry .time-type{display:flex;align-items:center;gap:9px}.time-dot{width:9px;height:9px;border-radius:50%;background:var(--primary)}.time-entry strong{font-size:11px}.time-entry span{font-size:10px;color:var(--muted)}
.tabs{display:flex;gap:3px;overflow:auto;padding:0 16px;border-bottom:1px solid var(--border)}.tab{min-height:43px;padding:0 10px;border:0;border-bottom:2px solid transparent;background:transparent;color:var(--muted);font-size:11px;font-weight:820;white-space:nowrap}.tab.active{color:var(--primary);border-bottom-color:var(--primary)}.tab-panel{display:none;padding:17px}.tab-panel.active{display:block}.table-wrap{overflow:auto;border:1px solid var(--border);border-radius:11px}.table{width:100%;min-width:690px;border-collapse:collapse}.table th,.table td{padding:10px 11px;border-bottom:1px solid var(--border);text-align:left;font-size:10px;vertical-align:middle}.table th{background:var(--surface-2);color:var(--faint);font-size:9px;text-transform:uppercase}.table tr:last-child td{border-bottom:0}
.search-form{padding:18px}.result-list{display:grid;gap:11px}.result{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:13px;align-items:center;padding:15px;border:1px solid var(--border);border-radius:14px;background:var(--surface)}.result h3{margin:7px 0 4px;font-size:14px}.result p{margin:0;color:var(--muted);font-size:11px;line-height:1.45}.result-meta{display:flex;gap:8px;flex-wrap:wrap;margin-top:8px;color:var(--faint);font-size:9px}
.master-detail{display:grid;grid-template-columns:280px minmax(0,1fr);gap:17px;align-items:start}.master{position:sticky;top:calc(var(--top) + 18px);overflow:hidden}.master-list{max-height:calc(100vh - 190px);overflow:auto}.master-item{width:100%;display:grid;gap:4px;padding:13px 15px;border:0;border-bottom:1px solid var(--border);background:var(--surface);text-align:left}.master-item:last-child{border-bottom:0}.master-item:hover{background:var(--surface-2)}.master-item.active{background:var(--primary-soft);box-shadow:inset 4px 0 var(--primary)}.master-item strong{font-size:11px}.master-item span,.master-item small{color:var(--muted);font-size:9px;line-height:1.35}.entity-head{display:flex;align-items:flex-start;justify-content:space-between;gap:14px;padding:20px;border-bottom:1px solid var(--border)}.entity-title{display:flex;gap:12px}.entity-icon{width:52px;height:52px;display:grid;place-items:center;flex:0 0 auto;border-radius:15px;background:var(--primary-soft);color:var(--primary);font-size:21px}.entity-head h1{margin:7px 0 4px;font-size:21px}.entity-head p{margin:0;color:var(--muted);font-size:11px;line-height:1.45}
.template-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:13px}.template-card{display:flex;flex-direction:column;gap:12px;padding:16px;border:1px solid var(--border);border-radius:15px;background:var(--surface)}.template-card:hover{border-color:var(--primary);box-shadow:var(--shadow)}.template-icon{width:46px;height:46px;display:grid;place-items:center;border-radius:13px;background:var(--primary-soft);color:var(--primary);font-size:20px}.template-card h3{margin:0;font-size:14px}.template-card p{margin:0;flex:1;color:var(--muted);font-size:11px;line-height:1.5}.template-card .btn{width:100%}
.dynamic{padding:20px}.dynamic h2{margin:0;font-size:18px}.dynamic>p{margin:5px 0 18px;color:var(--muted);font-size:11px}.report-layout{display:grid;grid-template-columns:minmax(0,1fr) 310px;gap:17px;align-items:start}.report-side{position:sticky;top:calc(var(--top) + 18px)}.signatures{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:13px}.signature{overflow:hidden;border:1px solid var(--border);border-radius:13px;background:var(--surface)}.signature-head{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px 12px;border-bottom:1px solid var(--border);font-size:11px;font-weight:820}.signature canvas{display:block;width:100%;height:150px;touch-action:none;background:var(--surface-2)}
.photos{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:13px}.photo{overflow:hidden;border:1px solid var(--border);border-radius:14px;background:var(--surface)}.photo img{display:block;width:100%;aspect-ratio:16/10;object-fit:cover;background:var(--surface-3)}.photo-body{display:grid;gap:9px;padding:12px}.photo-body strong{font-size:11px}.drop{min-height:110px;display:grid;place-items:center;gap:4px;padding:15px;border:2px dashed var(--border);border-radius:14px;background:var(--surface-2);color:var(--muted);text-align:center;cursor:pointer}.drop:hover{border-color:var(--primary);color:var(--primary)}
.email-layout{display:grid;grid-template-columns:minmax(0,1fr) 360px;gap:17px;align-items:start}.email-preview{position:sticky;top:calc(var(--top) + 18px);overflow:hidden}.mail-window{background:var(--surface-2)}.mail-bar{display:flex;gap:6px;padding:11px;border-bottom:1px solid var(--border)}.mail-dot{width:9px;height:9px;border-radius:50%;background:var(--border)}.mail-content{padding:16px}.mail-line{display:grid;grid-template-columns:55px 1fr;gap:8px;padding:8px 0;border-bottom:1px solid var(--border);font-size:10px}.mail-line span:first-child{color:var(--faint)}.mail-body{min-height:180px;padding-top:14px;color:var(--muted);font-size:11px;line-height:1.6;white-space:pre-wrap}.mode-card{padding:14px;border:2px solid var(--border);border-radius:14px;background:var(--surface);text-align:left}.mode-card.active{border-color:var(--primary);box-shadow:0 0 0 3px var(--primary-soft)}.mode-card strong{display:block;font-size:12px}.mode-card span{display:block;margin-top:4px;color:var(--muted);font-size:10px;line-height:1.4}
.theme-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:11px}.theme-choice{padding:12px;border:2px solid var(--border);border-radius:13px;background:var(--surface-2)}.theme-choice.active{border-color:var(--primary);box-shadow:0 0 0 3px var(--primary-soft)}.theme-thumb{height:95px;display:grid;grid-template-columns:26px 1fr;gap:6px;padding:8px;margin-bottom:8px;border-radius:9px}.theme-thumb.light{background:#edf1f7}.theme-thumb.dark{background:#0c1320}.theme-thumb:before{content:"";border-radius:5px;background:#4f46e5}.theme-thumb:after{content:"";border-radius:5px;background:#fff}.theme-thumb.dark:after{background:#1c283a}.swatches{display:flex;gap:9px;flex-wrap:wrap}.swatch{width:36px;height:36px;border:3px solid var(--surface);border-radius:50%;background:var(--c);box-shadow:0 0 0 1px var(--border)}.swatch.active{box-shadow:0 0 0 3px var(--surface),0 0 0 6px var(--c)}.preview{overflow:hidden}.preview-top{height:55px;display:flex;align-items:center;justify-content:space-between;padding:0 15px;background:var(--primary);color:var(--on-primary)}.preview-body{min-height:250px;padding:15px;background:var(--bg)}.preview-card{padding:13px;border:1px solid var(--border);border-radius:11px;background:var(--surface)}.line{height:8px;margin-bottom:8px;border-radius:99px;background:var(--border)}.line.short{width:45%}.preview-button{width:78px;height:26px;margin-top:12px;border-radius:7px;background:var(--primary)}
.empty{padding:38px 20px;text-align:center}.empty-icon{width:52px;height:52px;display:grid;place-items:center;margin:0 auto 12px;border-radius:16px;background:var(--primary-soft);color:var(--primary);font-size:22px}.empty h3{margin:0 0 6px}.empty p{margin:0 auto 15px;max-width:460px;color:var(--muted);font-size:12px;line-height:1.5}
.sheet,.modal-wrap{position:fixed;inset:0;z-index:80;display:grid;background:rgba(5,9,16,.58);opacity:0;pointer-events:none;transition:.18s}.sheet{align-items:end;padding:10px}.modal-wrap{place-items:center;padding:12px}.sheet.open,.modal-wrap.open{opacity:1;pointer-events:auto}.sheet-box{width:min(100%,500px);justify-self:center;padding:10px 13px 18px;border:1px solid var(--border);border-radius:22px 22px 15px 15px;background:var(--surface);transform:translateY(25px);transition:.18s}.sheet.open .sheet-box{transform:none}.sheet-handle{width:45px;height:5px;margin:1px auto 12px;border-radius:99px;background:var(--border)}.sheet-menu{display:grid;gap:7px}.sheet-menu button{min-height:47px;display:flex;align-items:center;gap:10px;padding:10px 12px;border:1px solid var(--border);border-radius:11px;background:var(--surface-2);text-align:left}.sheet-menu button span:last-child{margin-left:auto}.modal{width:min(100%,720px);max-height:92vh;overflow:auto;border:1px solid var(--border);border-radius:18px;background:var(--surface);box-shadow:var(--shadow)}.modal-head,.modal-foot{position:sticky;z-index:2;display:flex;align-items:center;justify-content:space-between;gap:10px;padding:15px 18px;background:var(--surface)}.modal-head{top:0;border-bottom:1px solid var(--border)}.modal-foot{bottom:0;justify-content:flex-end;border-top:1px solid var(--border)}.modal-head h2{margin:0;font-size:17px}.modal-body{padding:18px}
.toast-area{position:fixed;z-index:100;right:16px;bottom:16px;display:grid;gap:8px;width:min(370px,calc(100vw - 32px));pointer-events:none}.toast{display:flex;gap:10px;padding:12px 13px;border:1px solid var(--border);border-radius:12px;background:var(--surface);box-shadow:var(--shadow);animation:toastIn .2s ease}.toast strong{display:block;font-size:11px}.toast p{margin:3px 0 0;color:var(--muted);font-size:10px;line-height:1.4}@keyframes toastIn{from{opacity:0;transform:translateY(10px)}}.spin{display:inline-block;width:18px;height:18px;border:2px solid currentColor;border-right-color:transparent;border-radius:50%;animation:spin .75s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}
/* =========================================================
OMIKA — ÉCRANS SPÉCIALISÉS ET RELATIONS MÉTIER
========================================================= */
.relation-callout{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:15px;border:1px solid var(--border);border-radius:14px;background:var(--primary-soft)}
.relation-callout.neutral{background:var(--surface-2)}
.relation-callout h3{margin:6px 0 4px;font-size:15px}.relation-callout p{margin:0;color:var(--muted);font-size:12px;line-height:1.5}
.resource-card{min-height:150px;transition:.16s ease}.resource-card:hover{transform:translateY(-1px)}.resource-card b{margin-top:auto}
.fixed-document{overflow:hidden}.fixed-banner{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:16px 18px;border-bottom:1px solid var(--border);background:var(--surface-2)}
.fixed-banner strong,.fixed-banner span{display:block}.fixed-banner strong{font-size:14px}.fixed-banner div>span{margin-top:3px;color:var(--faint);font-size:10px}
.paper-meta{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1px;margin-bottom:18px;overflow:hidden;border:1px solid var(--border);border-radius:12px;background:var(--border)}
.paper-meta>div{padding:12px;background:var(--surface-2)}.paper-meta span,.paper-meta strong{display:block}.paper-meta span{color:var(--faint);font-size:9px;font-weight:800;text-transform:uppercase}.paper-meta strong{margin-top:4px;font-size:11px}
.form-section{padding:16px 0;border-top:1px solid var(--border)}.form-section:first-of-type{border-top:0}
.form-section-title{display:flex;align-items:center;gap:10px;margin-bottom:14px}.form-section-title>span{width:28px;height:28px;display:grid;place-items:center;border-radius:9px;background:var(--primary);color:var(--on-primary);font-size:11px;font-weight:900}.form-section-title strong,.form-section-title small{display:block}.form-section-title strong{font-size:13px}.form-section-title small{margin-top:2px;color:var(--faint);font-size:9px}
.email-layout{grid-template-columns:minmax(250px,.62fr) minmax(0,1.38fr)}
.email-mode-grid{display:grid;gap:9px}.email-mode{display:grid;grid-template-columns:32px minmax(0,1fr);gap:3px 10px;padding:12px;border:1px solid var(--border);border-radius:12px;background:var(--surface-2);text-align:left}
.email-mode>span{grid-row:1/3;width:32px;height:32px;display:grid;place-items:center;border-radius:9px;background:var(--surface);color:var(--primary)}.email-mode strong{font-size:11px}.email-mode small{color:var(--faint);font-size:9px;line-height:1.35}.email-mode.active{border-color:var(--primary);background:var(--primary-soft)}
.email-preview{position:static;top:auto;display:grid;gap:11px;padding:14px;border:1px dashed var(--border);border-radius:13px;background:var(--surface-2)}.email-preview textarea{min-height:210px}
.intervention-result{box-shadow:none}.time-entry{flex-wrap:wrap}
@media(max-width:1100px) and (min-width:720px){:root{--rail:80px}.brand{justify-content:center;padding-inline:0}.brand-copy,.nav-label,.nav span:not(.count),.nav .count,.rail-sync,.profile div:last-child{display:none}.nav button{justify-content:center;padding-inline:0}.profile{justify-content:center}.detail-layout,.report-layout,.email-layout{grid-template-columns:1fr}.detail-side,.report-side,.email-preview{position:static}.grid-4{grid-template-columns:repeat(2,1fr)}.master-detail{grid-template-columns:235px 1fr}.template-grid{grid-template-columns:repeat(2,1fr)}}
@media(max-width:719px){:root{--top:60px}.rail{display:none}.shell{margin-left:0}.topbar{padding:0 11px}.menu-btn{display:grid}.topbar small{display:none}.topbar h2{font-size:16px}.topbar-actions .btn span.label{display:none}main{padding:14px 11px 90px}.mobile-nav{position:fixed;inset:auto 0 0 0;z-index:40;display:grid;grid-template-columns:repeat(5,1fr);min-height:72px;padding:6px 5px calc(6px + env(safe-area-inset-bottom));border-top:1px solid var(--border);background:var(--surface)}.mobile-nav button{display:grid;place-items:center;gap:2px;padding:4px 2px;border-radius:10px;color:var(--faint);font-size:8px;font-weight:820}.mobile-nav button.active{background:var(--primary-soft);color:var(--primary)}.mobile-nav .ico{font-size:18px}.toast-area{right:10px;bottom:84px;width:calc(100vw - 20px)}.page-head{display:grid}.welcome{grid-template-columns:1fr;padding:18px}.grid-4{grid-template-columns:repeat(2,1fr)}.agenda-tools{align-items:stretch;flex-direction:column}.tool-group{justify-content:space-between}.period{min-width:0;flex:1;font-size:11px}.date-strip{gap:2px;padding:7px 4px}.date-chip{padding:7px 1px}.date-chip span{font-size:7px}.date-chip strong{font-size:14px}.appt{grid-template-columns:58px 1fr;gap:10px;padding:12px 10px 12px 12px}.appt-time strong{font-size:15px}.appt-action{grid-column:1/-1;padding-left:68px}.appt-action .btn{width:100%}.hero{padding:18px}.hero-top{display:grid}.hero-actions{display:grid;grid-template-columns:repeat(2,1fr)}.hero-actions .main{grid-column:1/-1}.detail-layout,.report-layout,.email-layout,.master-detail{grid-template-columns:1fr}.detail-side,.report-side,.email-preview,.master{position:static}.grid-2,.grid-3,.form-grid,.signatures,.theme-grid,.resource-grid,.template-grid{grid-template-columns:1fr}.span-2{grid-column:auto}.photos{grid-template-columns:1fr}.checks{grid-template-columns:1fr}.search-form{padding:14px}.result{grid-template-columns:1fr}.result .btn{width:100%}.master-list{display:flex;max-height:none;overflow:auto}.master-item{min-width:230px;border-right:1px solid var(--border);border-bottom:0}.master-item.active{box-shadow:inset 0 -4px var(--primary)}.entity-head{display:grid;padding:16px}.entity-head .actions{justify-content:flex-start}.card-body,.dynamic{padding:15px}.callout{display:grid}.callout .actions{margin-left:0}.time-summary{grid-template-columns:1fr}.time-entry{grid-template-columns:1fr auto}.time-entry .time-type{grid-column:1/-1}.time-entry .duration-cell{justify-self:start}.table{min-width:640px}}
@media(max-width:400px){.grid-4{grid-template-columns:1fr}.hero-actions{grid-template-columns:1fr}.hero-actions .main{grid-column:auto}.appt-action{padding-left:0}.topbar-actions .btn{display:none}}
@media(max-width:719px){
.relation-callout{align-items:stretch;flex-direction:column}.relation-callout .btn{width:100%}
.paper-meta{grid-template-columns:1fr}.fixed-banner{align-items:flex-start;flex-direction:column}
.email-mode-grid{grid-template-columns:1fr}.resource-card{min-height:125px}
.master-detail,.master-detail>*{min-width:0}.master{width:100%;max-width:100%}.master-list{width:100%;max-width:100%}
.entity-title,.entity-title>div:last-child{min-width:0}.entity-head h1,.entity-head p{overflow-wrap:anywhere}
}
@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important;animation-duration:.01ms!important;transition-duration:.01ms!important}}
</style>
</head>
<body data-theme="light">
<div class="app">
<!-- Navigation latérale sur desktop, compacte sur tablette -->
<aside class="rail">
<div class="brand"><div class="brand-mark">OM</div><div class="brand-copy"><h1>Omika</h1><p>Interventions terrain</p></div></div>
<div class="nav-label">Espace de travail</div>
<nav class="nav" id="sideNav">
<button data-route="home"><i class="ico">▣</i><span>Mes rendez-vous</span><b class="count" id="navCount">0</b></button>
<button data-route="team"><i class="ico">◎</i><span>Équipe</span></button>
<button data-route="search"><i class="ico">⌕</i><span>Interventions</span></button>
<button data-route="folders"><i class="ico">▤</i><span>Dossiers</span></button>
<button data-route="clients"><i class="ico">▥</i><span>Clients</span></button>
<button data-route="locations"><i class="ico">⌖</i><span>Lieux</span></button>
<button data-route="settings"><i class="ico">⚙</i><span>Paramètres</span></button>
</nav>
<div class="rail-spacer"></div>
<div class="rail-sync"><strong id="railSyncTitle">Données à jour</strong><br><span id="railSyncTime">Synchronisé récemment</span></div>
<div class="profile"><div class="avatar">CB</div><div><strong>Camille Bernard</strong><span>Technicienne CVC</span></div></div>
</aside>
<section class="shell">
<header class="topbar">
<div class="topbar-left">
<button class="icon-btn menu-btn" id="menuButton" aria-label="Ouvrir le menu">☰</button>
<div><small>Omika · Espace terrain</small><h2 id="screenTitle">Mes rendez-vous</h2></div>
</div>
<div class="topbar-actions">
<button class="btn btn-secondary" id="syncTop"><i class="ico">↻</i><span class="label">Synchroniser</span></button>
<button class="icon-btn" id="themeTop" aria-label="Changer de thème">☾</button>
</div>
</header>
<main id="view" aria-live="polite"></main>
<nav class="mobile-nav" id="mobileNav">
<button data-route="home"><i class="ico">▣</i><span>Agenda</span></button>
<button data-route="team"><i class="ico">◎</i><span>Équipe</span></button>
<button data-route="search"><i class="ico">⌕</i><span>Interventions</span></button>
<button data-route="folders"><i class="ico">▤</i><span>Dossiers</span></button>
<button id="moreButton"><i class="ico">•••</i><span>Plus</span></button>
</nav>
</section>
</div>
<!-- Menu complémentaire sur smartphone -->
<div class="sheet" id="moreSheet" aria-hidden="true">
<div class="sheet-box">
<div class="sheet-handle"></div>
<div class="cluster" style="margin-bottom:10px"><h2 style="margin:0;font-size:17px">Plus</h2><button class="icon-btn" id="closeSheet" aria-label="Fermer">×</button></div>
<div class="sheet-menu">
<button data-route="clients"><span>▥ &nbsp; Fiches clients</span><span>›</span></button>
<button data-route="locations"><span>⌖ &nbsp; Fiches lieux</span><span>›</span></button>
<button data-route="settings"><span>⚙ &nbsp; Paramètres</span><span>›</span></button>
<button id="syncMobile"><span>↻ &nbsp; Synchroniser maintenant</span><span>›</span></button>
</div>
</div>
</div>
<!-- Création d'une intervention : elle existe indépendamment de tout rendez-vous -->
<div class="modal-wrap" id="modalWrap" aria-hidden="true">
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="interventionModalTitle">
<div class="modal-head">
<div><span class="badge primary">Intervention</span><h2 id="interventionModalTitle" style="margin-top:7px">Nouvelle intervention</h2></div>
<button class="icon-btn" type="button" data-close-modal aria-label="Fermer">×</button>
</div>
<form id="newInterventionForm">
<div class="modal-body stack-lg">
<div class="relation-callout neutral">
<div><strong>Création indépendante</strong><p>Cette action crée uniquement une intervention. Un ou plusieurs rendez-vous pourront ensuite être planifiés depuis sa fiche.</p></div>
</div>
<div class="form-grid">
<div class="field span-2"><label class="required">Objet de l'intervention</label><input name="title" required placeholder="Ex. Diagnostic du groupe froid GF-02"></div>
<div class="field"><label class="required">Dossier</label><select name="folder" id="modalFolder" required></select></div>
<div class="field"><label>Priorité</label><select name="priority"><option>Normale</option><option>Haute</option><option>Urgente</option></select></div>
<div class="field"><label class="required">Client</label><select name="client" id="modalClient" required></select></div>
<div class="field"><label class="required">Lieu</label><select name="location" id="modalLocation" required></select></div>
<div class="field span-2"><label class="required">Collaborateur référent</label><select name="owner" id="modalOwner" required></select></div>
<div class="field span-2"><label>Description</label><textarea name="description" placeholder="Contexte, équipement concerné, consignes et résultat attendu…"></textarea></div>
</div>
</div>
<div class="modal-foot"><button type="button" class="btn btn-secondary" data-close-modal>Annuler</button><button class="btn btn-primary">+ Créer l'intervention</button></div>
</form>
</div>
</div>
<!-- Planification facultative d'un rendez-vous depuis une intervention -->
<div class="modal-wrap" id="appointmentModalWrap" aria-hidden="true">
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="appointmentModalTitle">
<div class="modal-head">
<div><span class="badge primary">Rendez-vous</span><h2 id="appointmentModalTitle" style="margin-top:7px">Planifier un rendez-vous</h2></div>
<button class="icon-btn" type="button" data-close-appointment-modal aria-label="Fermer">×</button>
</div>
<form id="newAppointmentForm">
<input type="hidden" name="intervention" id="appointmentIntervention">
<div class="modal-body stack-lg">
<div class="relation-callout" id="appointmentContext"></div>
<div class="form-grid">
<div class="field"><label class="required">Date</label><input type="date" name="date" id="appointmentDate" required></div>
<div class="field"><label class="required">Collaborateur</label><select name="collaborator" id="appointmentCollaborator" required></select></div>
<div class="field"><label class="required">Heure de début</label><input type="time" name="from" id="appointmentFrom" required></div>
<div class="field"><label class="required">Heure de fin</label><input type="time" name="to" id="appointmentTo" required></div>
<div class="field span-2"><label>Description du rendez-vous</label><textarea name="description" placeholder="Objectif du déplacement, personne à rencontrer, consignes…"></textarea></div>
</div>
</div>
<div class="modal-foot"><button type="button" class="btn btn-secondary" data-close-appointment-modal>Annuler</button><button class="btn btn-primary">▣ Planifier le rendez-vous</button></div>
</form>
</div>
</div>
<div class="toast-area" id="toasts"></div>
<script>
/* =========================================================
OMIKA — DONNÉES FICTIVES ET MAQUETTE FONCTIONNELLE
Les rendez-vous et les interventions sont volontairement
stockés dans deux collections séparées.
========================================================= */
const $=s=>document.querySelector(s), $$=s=>[...document.querySelectorAll(s)], id=s=>document.getElementById(s);
const pad=n=>String(n).padStart(2,"0"), start=d=>new Date(d.getFullYear(),d.getMonth(),d.getDate());
const add=(d,n)=>{const x=new Date(d);x.setDate(x.getDate()+n);return start(x)}, weekStart=d=>{const x=start(d),day=x.getDay()||7;return add(x,1-day)};
const iso=d=>`${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}`, parse=s=>{const [y,m,d]=String(s).split("-").map(Number);return new Date(y,m-1,d)};
const fmt=(d,o)=>new Intl.DateTimeFormat("fr-FR",o).format(d), esc=v=>String(v??"").replace(/[&<>'"]/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;","'":"&#39;",'"':"&quot;"}[c]));
const norm=v=>String(v??"").normalize("NFD").replace(/[\u0300-\u036f]/g,"").toLowerCase().trim();
const today=start(new Date()), rangeStart=add(today,-14), rangeEnd=add(today,28);
const initials=n=>String(n).split(/\s+/).slice(0,2).map(x=>x[0]).join("").toUpperCase();
const timeToMinutes=t=>{const [h,m]=String(t||"00:00").split(":").map(Number);return h*60+m};
const minutesToTime=m=>{const v=(m+1440)%1440;return `${pad(Math.floor(v/60))}:${pad(v%60)}`};
const duration=(a,b)=>Math.max(0,timeToMinutes(b)-timeToMinutes(a));
const humanDuration=m=>m<60?`${m} min`:`${Math.floor(m/60)} h ${pad(m%60)}`;
const statusInfo={planned:["Planifié","planned",""],progress:["En cours","progress","is-progress"],completed:["Terminé","completed","is-completed"],late:["À replanifier","late","is-late"]};
const status=s=>{const x=statusInfo[s]||statusInfo.planned;return`<span class="status ${x[1]}">${x[0]}</span>`};
const timeTypeInfo={intervention:["Intervention","⚒"],transport:["Transport","↗"],lunch:["Pause déjeuner","☕"],administrative:["Administratif","▤"],other:["Autre","◷"]};
const tone=n=>["#446a92","#7657c8","#28766e","#a15b38","#587337"][[...String(n)].reduce((a,c)=>a+c.charCodeAt(0),0)%5];
const collaborators=[
{id:"c1",name:"Camille Bernard",role:"Technicienne CVC",phone:"0612457801",email:"camille.bernard@omika-demo.test",color:"#7657c8"},
{id:"c2",name:"Sami El Amrani",role:"Technicien multitechnique",phone:"0678421190",email:"sami.elamrani@omika-demo.test",color:"#28766e"},
{id:"c3",name:"Inès Morel",role:"Chargée de maintenance",phone:"0622147896",email:"ines.morel@omika-demo.test",color:"#b45f2c"},
{id:"c4",name:"Hugo Lefèvre",role:"Expert électricité",phone:"0698167544",email:"hugo.lefevre@omika-demo.test",color:"#2a67b1"}
];
const clients=[
{id:"CLI-001",name:"Novalys Logistique",legal:"Novalys Logistique SAS",ref:"CL-00482",sector:"Transport & logistique",phone:"0145826710",email:"maintenance@novalys-logistique.test",address:"24 avenue des Nations, 93290 Tremblay-en-France",note:"Contrat premium. Prévenir le responsable du site 30 minutes avant l'arrivée.",contacts:[{name:"Marc Duval",role:"Responsable maintenance",phone:"0612784532",email:"marc.duval@novalys-logistique.test"},{name:"Lina Ribeiro",role:"Accueil sécurité",phone:"0145826714",email:"securite@novalys-logistique.test"}],docs:["Contrat maintenance 2026.pdf","Procédure sécurité site.pdf"]},
{id:"CLI-002",name:"Clinique Saint-Martin",legal:"Clinique Saint-Martin",ref:"CL-00318",sector:"Santé",phone:"0142769030",email:"services.techniques@clinique-stmartin.test",address:"8 rue des Tilleuls, 92100 Boulogne-Billancourt",note:"Interventions silencieuses entre 13 h et 15 h. Accès par l'entrée fournisseurs.",contacts:[{name:"Élodie Martin",role:"Responsable services techniques",phone:"0674201833",email:"elodie.martin@clinique-stmartin.test"},{name:"Poste sécurité",role:"Accès site",phone:"0142769039",email:"securite@clinique-stmartin.test"}],docs:["Plan des zones techniques.pdf","Consignes hygiène.pdf"]},
{id:"CLI-003",name:"Atelier MécaPro",legal:"MécaPro Industries SARL",ref:"CL-00504",sector:"Industrie",phone:"0139557712",email:"atelier@mecapro.test",address:"17 rue des Forges, 95100 Argenteuil",note:"Port des EPI obligatoire. Stock de pièces dans l'armoire technique B12.",contacts:[{name:"Romain Petit",role:"Chef d'atelier",phone:"0609812476",email:"romain.petit@mecapro.test"},{name:"Sofia Benali",role:"Achats",phone:"0139557721",email:"achats@mecapro.test"}],docs:["Registre équipements.xlsx","Plan atelier.pdf"]},
{id:"CLI-004",name:"Hôtel Lumière",legal:"Hôtel Lumière Opéra SAS",ref:"CL-00275",sector:"Hôtellerie",phone:"0144206100",email:"direction.technique@hotel-lumiere.test",address:"12 boulevard des Capucines, 75009 Paris",note:"Éviter les travaux bruyants avant 10 h. Parking livraison rue Caumartin.",contacts:[{name:"Julien Caron",role:"Directeur technique",phone:"0661349812",email:"julien.caron@hotel-lumiere.test"},{name:"Réception",role:"Accueil",phone:"0144206100",email:"reception@hotel-lumiere.test"}],docs:["Liste des chambres techniques.pdf","Protocole d'accès.pdf"]},
{id:"CLI-005",name:"Groupe Arcadia",legal:"Arcadia Services France",ref:"CL-00611",sector:"Services",phone:"0184302250",email:"facilities@arcadia-services.test",address:"2 place des Reflets, 92400 Courbevoie",note:"Badges visiteurs au niveau -1. Appeler le facility manager en cas de retard.",contacts:[{name:"Nora Bensaïd",role:"Facility manager",phone:"0677114520",email:"nora.bensaid@arcadia-services.test"},{name:"PC sécurité",role:"Accès tour",phone:"0184302299",email:"pcs@arcadia-services.test"}],docs:["Convention d'intervention.pdf","Plan technique niveaux.pdf"]}
];
const locations=[
{id:"LOC-001",client:"CLI-001",name:"Plateforme logistique Nord",address:"24 avenue des Nations, 93290 Tremblay-en-France",access:"Entrée poids lourds B, présenter la référence d'intervention.",note:"Local technique au quai 18. Réseau mobile faible dans la zone froide.",docs:["Plan plateforme Nord.pdf"]},
{id:"LOC-002",client:"CLI-002",name:"Bâtiment principal",address:"8 rue des Tilleuls, 92100 Boulogne-Billancourt",access:"Entrée fournisseurs, badge temporaire au poste de sécurité.",note:"Groupes de traitement d'air en toiture, accès par l'escalier T3.",docs:["Plan toiture technique.pdf"]},
{id:"LOC-003",client:"CLI-003",name:"Atelier de production",address:"17 rue des Forges, 95100 Argenteuil",access:"Portail 2, stationnement sur les places visiteurs.",note:"Consignation électrique requise avant ouverture des armoires.",docs:["Schéma électrique atelier.pdf"]},
{id:"LOC-004",client:"CLI-004",name:"Hôtel Lumière Opéra",address:"12 boulevard des Capucines, 75009 Paris",access:"Accès technique rue Caumartin, prévenir la réception.",note:"Machinerie principale au sous-sol -2.",docs:["Plan sous-sol technique.pdf"]},
{id:"LOC-005",client:"CLI-005",name:"Tour Arcadia — La Défense",address:"2 place des Reflets, 92400 Courbevoie",access:"Accueil visiteurs niveau -1, pièce d'identité obligatoire.",note:"Local CVC au 23e étage. Monte-charge sur réservation.",docs:["Plan d'accès tour.pdf"]},
{id:"LOC-006",client:"CLI-001",name:"Agence logistique Sud",address:"6 rue de la Plaine, 94550 Chevilly-Larue",access:"Portail livraison, code 8241.",note:"Intervenir avant 15 h pour éviter le pic d'activité.",docs:["Plan agence Sud.pdf"]}
];
const interventionSeeds=[
["Maintenance préventive du groupe froid","Contrôle préventif, relevés de températures et vérification des sécurités.","Effectuer le contrôle complet du groupe froid, nettoyer les filtres, relever les pressions et tester les alarmes."],
["Diagnostic défaut de ventilation","Recherche de panne sur l'unité de ventilation et remise en service.","Identifier l'origine du défaut intermittent, contrôler le variateur, les sondes et le câblage."],
["Contrôle des armoires électriques","Inspection visuelle, serrage et mesures thermiques.","Contrôler les départs électriques, les couples de serrage et relever les points chauds."],
["Remplacement d'une sonde de température","Dépose de la sonde défectueuse, pose et calibration.","Remplacer la sonde, contrôler la cohérence de mesure et valider la remontée dans la supervision."],
["Visite de conformité annuelle","Contrôle réglementaire et mise à jour du registre.","Vérifier l'état général, relever les non-conformités et mettre à jour le registre de maintenance."],
["Réglage du système de régulation","Optimisation des consignes et des plages horaires.","Ajuster les consignes, vérifier les séquences de démarrage et réduire les dérives de consommation."]
];
const standaloneAppointmentSeeds=[
["Réunion de coordination","Point d'organisation avec le responsable du site avant lancement des travaux."],
["Visite technique sans dossier","Relevé initial sur site avant la création éventuelle d'une intervention."],
["Remise de documents","Échange de documents et validation des accès avec le client."],
["Audit de site","Observation générale du site sans intervention technique associée."]
];
function fakeImage(label,a="#315a75",b="#7f9c87"){
const svg=`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 500"><defs><linearGradient id="g"><stop stop-color="${a}"/><stop offset="1" stop-color="${b}"/></linearGradient></defs><rect width="800" height="500" fill="url(#g)"/><rect x="80" y="70" width="640" height="340" rx="25" fill="rgba(255,255,255,.16)" stroke="rgba(255,255,255,.4)" stroke-width="5"/><circle cx="230" cy="240" r="72" fill="rgba(255,255,255,.22)"/><path d="M350 165h250v30H350zM350 220h190v24H350zM350 270h230v24H350zM350 320h150v24H350z" fill="rgba(255,255,255,.55)"/><text x="400" y="458" text-anchor="middle" fill="white" font-family="Arial" font-size="28" font-weight="700">${label}</text></svg>`;
return`data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
}
function buildInterventions(){
const list=[];
for(let i=0;i<36;i++){
const client=clients[i%clients.length], clientLocations=locations.filter(l=>l.client===client.id), location=clientLocations[i%clientLocations.length], seed=interventionSeeds[i%interventionSeeds.length];
const opened=add(today,-58+i*3), st=i%9===0?"late":i%5===0?"completed":i%4===0?"progress":"planned";
const reportSample=i%8===0?[{id:`RAP-${today.getFullYear()}-${String(301+i).padStart(4,"0")}`,templateId:"maintenance",title:"Rapport de maintenance préventive",createdAt:iso(add(today,-(i%12+1))),status:"Créé"}]:[];
list.push({
id:`INT-${today.getFullYear()}-${String(1101+i).padStart(4,"0")}`,
client:client.id,location:location.id,owner:collaborators[i%collaborators.length].id,status:st,priority:i%7===0?"Urgente":i%3===0?"Haute":"Normale",
title:seed[0],short:seed[1],detail:seed[2],opened:iso(opened),due:iso(add(opened,24+i%15)),
internalSummary:i%2?"Prévoir le contrôle des pièces de rechange avant déplacement.":"Le client souhaite recevoir un retour écrit après les opérations.",
notes:[{author:"Planification",date:iso(add(opened,1)),text:"Vérifier les accès, le matériel de mesure et les consignes de sécurité avant l'intervention."}],
documents:[["Bon d'intervention.pdf","PDF · 184 Ko"],["Fiche équipement.pdf","PDF · 426 Ko"]],
images:i%5===0?[{id:`img-int-${i}-1`,name:"Vue générale de l'équipement",url:fakeImage("Vue générale"),note:"État observé avant les opérations."},{id:`img-int-${i}-2`,name:"Plaque signalétique",url:fakeImage("Plaque signalétique","#6f4c86","#b06c61"),note:"Référence à conserver dans le dossier."}]:[],
reports:reportSample,
compteRendu:i%10===0?{status:"Validé",createdAt:iso(add(today,-(i%9+2))),reference:`CR-${today.getFullYear()}-${String(80+i).padStart(4,"0")}`}:null
});
}
return list;
}
const interventions=buildInterventions();
let timeEntrySeq=1;
function seededTimeEntries(st,from,to,index){
if(!["completed","progress"].includes(st))return[];
const out=[{id:`TEM-${timeEntrySeq++}`,type:"transport",start:minutesToTime(timeToMinutes(from)-35),end:from,note:"Trajet vers le site"}];
const interventionEnd=st==="progress"?minutesToTime(Math.min(timeToMinutes(to),timeToMinutes(from)+55)):to;
out.push({id:`TEM-${timeEntrySeq++}`,type:"intervention",start:from,end:interventionEnd,note:"Temps technique sur site"});
if(index%4===2)out.push({id:`TEM-${timeEntrySeq++}`,type:"lunch",start:"12:20",end:"13:00",note:"Pause déjeuner"});
return out;
}
function buildAppointments(){
const data=[];let seq=1800,linkCursor=0;const times=[["08:00","09:30"],["10:15","11:45"],["13:30","15:00"],["15:30","17:00"]],linkPool=interventions.slice(0,24);
for(let off=-14;off<=28;off++){
const d=add(today,off),day=d.getDay();if(day===0||(day===6&&off%2))continue;
const count=off===0?3:((Math.abs(off)+day)%5===0?2:1);
for(let j=0;j<count;j++){
seq++;
const standalone=(off===0&&j===2)||seq%5===0;
const linked=standalone?null:linkPool[(Math.abs(off*3+j)+linkCursor++)%linkPool.length];
const client=linked?clients.find(c=>c.id===linked.client):clients[Math.abs(off+j)%clients.length];
const clientLocations=locations.filter(l=>l.client===client.id),location=linked?locations.find(l=>l.id===linked.location):clientLocations[Math.abs(off+j)%clientLocations.length];
const collaborator=off===0?collaborators[0]:collaborators[Math.abs(off+j*2)%collaborators.length],[from,to]=times[j%times.length];
let st=off<0?"completed":"planned";if(off===0)st=["completed","progress","planned"][j];if(off>0&&off%11===0&&!j)st="late";
const standaloneSeed=standaloneAppointmentSeeds[Math.abs(off+j)%standaloneAppointmentSeeds.length];
data.push({
id:`RDV-${String(seq).padStart(5,"0")}`,interventionId:linked?.id||null,date:iso(d),from,to,client:client.id,location:location.id,collaborator:collaborator.id,status:st,
title:linked?linked.title:standaloneSeed[0],description:linked?"Rendez-vous planifié pour avancer sur l'intervention liée.":standaloneSeed[1],
timeEntries:seededTimeEntries(st,from,to,j+Math.abs(off))
});
}
}
return data.sort((a,b)=>(a.date+a.from).localeCompare(b.date+b.from));
}
const appointments=buildAppointments();
const folders=[
{id:"DOS-2026-018",title:"Modernisation de la chaîne du froid",client:"CLI-001",status:"En cours",priority:"Haute",manager:"Claire Robert",opened:iso(add(today,-34)),due:iso(add(today,24)),summary:"Programme de fiabilisation des équipements frigorifiques des plateformes Nord et Sud.",locations:["LOC-001","LOC-006"],docs:["Cahier des charges V3.pdf","Planning de déploiement.xlsx","Compte rendu de lancement.pdf"],interventions:interventions.filter(x=>x.client==="CLI-001").slice(0,7).map(x=>x.id)},
{id:"DOS-2026-024",title:"Mise en conformité ventilation bloc B",client:"CLI-002",status:"À surveiller",priority:"Urgente",manager:"Nadia Leroy",opened:iso(add(today,-18)),due:iso(add(today,11)),summary:"Levée des réserves de contrôle et réglage des débits d'air du bloc B.",locations:["LOC-002"],docs:["Rapport de contrôle initial.pdf","Plan aéraulique.pdf"],interventions:interventions.filter(x=>x.client==="CLI-002").slice(0,7).map(x=>x.id)},
{id:"DOS-2026-031",title:"Contrat maintenance multitechnique 2026",client:"CLI-005",status:"En cours",priority:"Normale",manager:"Alexandre Vial",opened:iso(add(today,-52)),due:iso(add(today,120)),summary:"Suivi annuel des installations CVC, électriques et de sécurité de la tour Arcadia.",locations:["LOC-005"],docs:["Contrat signé.pdf","Matrice des équipements.xlsx"],interventions:interventions.filter(x=>x.client==="CLI-005").slice(0,7).map(x=>x.id)}
];
const reportTemplates=[
{id:"maintenance",name:"Rapport de maintenance préventive",icon:"⚒",description:"Contrôles périodiques, mesures, pièces et recommandations.",fields:[
{type:"text",name:"equipment",label:"Équipement contrôlé",required:true,placeholder:"Ex. Groupe froid GF-02"},
{type:"text",name:"serial",label:"Numéro de série",placeholder:"Ex. SN-84A-2081"},
{type:"select",name:"condition",label:"État général",required:true,options:["Bon","Satisfaisant","Dégradé","Critique"]},
{type:"checks",name:"operations",label:"Opérations effectuées",span:true,options:["Contrôle visuel","Nettoyage","Mesures électriques","Relevé de températures","Test des sécurités","Réglage des consignes"]},
{type:"textarea",name:"measurements",label:"Mesures et relevés",span:true,placeholder:"Pressions, températures, intensités…"},
{type:"textarea",name:"recommendations",label:"Recommandations",required:true,span:true,placeholder:"Préconisations et échéances…"}
]},
{id:"diagnostic",name:"Rapport de diagnostic CVC",icon:"⌕",description:"Symptômes, tests, cause probable et solution recommandée.",fields:[
{type:"textarea",name:"symptoms",label:"Symptômes constatés",required:true,span:true,placeholder:"Décrivez les défauts observés…"},
{type:"checks",name:"tests",label:"Tests réalisés",span:true,options:["Alimentation","Régulation","Sondes","Débits","Pressions","Communication GTB"]},
{type:"select",name:"cause",label:"Cause identifiée",required:true,options:["Électrique","Mécanique","Régulation","Usage","À confirmer"]},
{type:"text",name:"faultCode",label:"Code défaut",placeholder:"Ex. E-214"},
{type:"textarea",name:"analysis",label:"Analyse technique",required:true,span:true,placeholder:"Expliquez le diagnostic…"},
{type:"textarea",name:"solution",label:"Solution proposée",required:true,span:true,placeholder:"Action immédiate et suite à prévoir…"}
]},
{id:"commissioning",name:"Rapport de mise en service",icon:"✓",description:"Paramètres de démarrage, essais et validation de fonctionnement.",fields:[
{type:"text",name:"equipment",label:"Équipement mis en service",required:true},
{type:"date",name:"commissionDate",label:"Date de mise en service",required:true},
{type:"checks",name:"tests",label:"Essais validés",span:true,options:["Mise sous tension","Sens de rotation","Sécurités","Alarmes","Régulation","Communication"]},
{type:"select",name:"result",label:"Résultat",required:true,options:["Mise en service validée","Validée avec réserves","Non validée"]},
{type:"textarea",name:"settings",label:"Réglages appliqués",span:true,placeholder:"Consignes et paramètres principaux…"},
{type:"textarea",name:"reservations",label:"Réserves ou actions restantes",span:true,placeholder:"Listez les points à traiter…"}
]},
{id:"safety",name:"Rapport de contrôle sécurité",icon:"!",description:"Vérifications de conformité, écarts et plan d'actions.",fields:[
{type:"select",name:"scope",label:"Périmètre",required:true,options:["Électricité","Incendie","Accès","Machines","Équipements sous pression"]},
{type:"checks",name:"controls",label:"Contrôles réalisés",span:true,options:["Signalisation","Protections","Arrêts d'urgence","Consignation","Documents réglementaires","EPI"]},
{type:"textarea",name:"deviations",label:"Écarts constatés",required:true,span:true},
{type:"select",name:"severity",label:"Niveau de risque",required:true,options:["Faible","Modéré","Élevé","Critique"]},
{type:"textarea",name:"actions",label:"Plan d'actions",required:true,span:true}
]}
];
const emailTemplates=[
{id:"report",name:"Envoi d'un rapport",description:"Transmettre un rapport créé avec un message contextualisé.",fields:[
{name:"contactName",label:"Nom du destinataire",required:true,placeholder:"Ex. Marc Duval"},
{name:"message",label:"Message d'accompagnement",type:"textarea",required:true,placeholder:"Précisez les éléments importants…"},
{name:"replyDate",label:"Date de retour souhaitée",type:"date"}
],build:(i,d,report)=>({subject:`Rapport ${report?.id||"d'intervention"} — ${i.id}`,body:`Bonjour ${d.contactName||""},\n\nVeuillez trouver ci-joint le rapport « ${report?.title||"rapport d'intervention"} » relatif à l'intervention ${i.id}.\n\n${d.message||""}${d.replyDate?`\n\nMerci de nous faire un retour avant le ${fmt(parse(d.replyDate),{day:"numeric",month:"long",year:"numeric"})}.`:""}\n\nCordialement,\nCamille Bernard\nOmika`})},
{id:"appointment",name:"Confirmation de rendez-vous",description:"Confirmer une date, un horaire et les conditions d'accès.",fields:[
{name:"contactName",label:"Nom du destinataire",required:true},
{name:"appointmentDate",label:"Date du rendez-vous",type:"date",required:true},
{name:"appointmentTime",label:"Heure",type:"time",required:true},
{name:"accessInfo",label:"Informations d'accès",type:"textarea",required:true}
],build:(i,d)=>({subject:`Confirmation de rendez-vous — ${i.id}`,body:`Bonjour ${d.contactName||""},\n\nNous vous confirmons le rendez-vous lié à l'intervention ${i.id} le ${d.appointmentDate?fmt(parse(d.appointmentDate),{weekday:"long",day:"numeric",month:"long",year:"numeric"}):""} à ${d.appointmentTime||""}.\n\nAccès et préparation : ${d.accessInfo||""}\n\nCordialement,\nCamille Bernard\nOmika`})},
{id:"followup",name:"Demande d'informations",description:"Demander des compléments avant la poursuite d'une intervention.",fields:[
{name:"contactName",label:"Nom du destinataire",required:true},
{name:"requestedItems",label:"Informations demandées",type:"textarea",required:true},
{name:"deadline",label:"Échéance souhaitée",type:"date",required:true}
],build:(i,d)=>({subject:`Informations requises — intervention ${i.id}`,body:`Bonjour ${d.contactName||""},\n\nAfin de poursuivre l'intervention ${i.id}, nous avons besoin des éléments suivants :\n${d.requestedItems||""}\n\nPouvez-vous nous les transmettre avant le ${d.deadline?fmt(parse(d.deadline),{day:"numeric",month:"long",year:"numeric"}):""} ?\n\nMerci par avance.\n\nCordialement,\nCamille Bernard\nOmika`})}
];
const getClient=x=>clients.find(c=>c.id===x), getLocation=x=>locations.find(l=>l.id===x), getCollab=x=>collaborators.find(c=>c.id===x), getAppt=x=>appointments.find(a=>a.id===x), getIntervention=x=>interventions.find(i=>i.id===x);
const appointmentsFor=i=>appointments.filter(a=>a.interventionId===i.id).sort((a,b)=>(a.date+a.from).localeCompare(b.date+b.from));
const storage=(()=>{const memory=new Map();try{const key="__omika_storage_test__";localStorage.setItem(key,"1");localStorage.removeItem(key);return localStorage}catch(_){return{getItem:key=>memory.has(key)?memory.get(key):null,setItem:(key,value)=>memory.set(key,String(value)),removeItem:key=>memory.delete(key)}}})();
const firstToday=appointments.find(a=>a.date===iso(today)&&a.collaborator==="c1")||appointments[0];
const state={
screen:"home",origin:"home",navStack:[],date:today,agendaView:"day",appointment:firstToday.id,intervention:firstToday.interventionId||interventions[0].id,resource:"notes",
reportTemplate:reportTemplates[0].id,emailMode:"direct",emailReport:null,teamCollab:"all",teamView:"list",teamWeek:weekStart(today),folder:folders[0].id,client:clients[0].id,location:locations[0].id,search:{relation:"all"},
theme:storage.getItem("omika-theme")||"light",primary:storage.getItem("omika-primary")||"#4f46e5",syncing:false,lastSync:new Date(Date.now()-240000),signatures:{tech:false,client:false}
};
/* =========================================================
OUTILS D'INTERFACE, THÈME, ROUTAGE ET SYNCHRONISATION
========================================================= */
function mix(hex,target,ratio){const p=v=>v.replace("#","").match(/../g).map(x=>parseInt(x,16)),a=p(hex),b=p(target),m=(x,y)=>Math.round(x+(y-x)*ratio).toString(16).padStart(2,"0");return`#${m(a[0],b[0])}${m(a[1],b[1])}${m(a[2],b[2])}`}
function contrast(hex){const [r,g,b]=hex.replace("#","").match(/../g).map(x=>parseInt(x,16));return(.299*r+.587*g+.114*b)>155?"#172033":"#ffffff"}
function applyTheme(theme=state.theme,primary=state.primary){state.theme=theme;state.primary=primary;document.body.dataset.theme=theme;const root=document.documentElement;root.style.setProperty("--primary",primary);root.style.setProperty("--primary-dark",mix(primary,theme==="dark"?"#ffffff":"#000000",theme==="dark"?.18:.28));root.style.setProperty("--primary-soft",mix(primary,theme==="dark"?"#141d2b":"#ffffff",theme==="dark"?.78:.9));root.style.setProperty("--on-primary",contrast(primary));id("themeMeta").content=primary;id("themeTop").textContent=theme==="dark"?"☀":"☾"}
function toast(title,message,type="info"){const el=document.createElement("div");el.className="toast";el.innerHTML=`<div class="row-icon">${type==="ok"?"✓":type==="error"?"!":"i"}</div><div><strong>${esc(title)}</strong><p>${esc(message)}</p></div>`;id("toasts").append(el);setTimeout(()=>{el.style.opacity=0;setTimeout(()=>el.remove(),200)},3300)}
function hashFor(screen,param){return param?`#${screen}/${encodeURIComponent(param)}`:`#${screen}`}
function navigate(screen,param,{push=true,clear=false}={}){closeSheet();if(clear)state.navStack=[];if(push)state.navStack.push(location.hash||"#home");const h=hashFor(screen,param);if(location.hash===h)render();else location.hash=h}
function goBack(){const h=state.navStack.pop()||`#${state.origin||"home"}`;if(location.hash===h)render();else location.hash=h}
function parseRoute(){const raw=location.hash.replace(/^#/,"")||"home",p=raw.indexOf("/");return{screen:p<0?raw:raw.slice(0,p),param:p<0?null:decodeURIComponent(raw.slice(p+1))}}
function compound(param){return String(param||"").split("|")}
function openAction(type,value){if(type==="gps"){window.open(`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(value)}`,"_blank","noopener");toast("Itinéraire préparé",value)}if(type==="phone")location.href=`tel:${String(value).replace(/\s/g,"")}`;if(type==="email")location.href=`mailto:${value}`}
function titles(s){return({home:"Mes rendez-vous",appointment:"Détail du rendez-vous",intervention:"Intervention",resource:"Contenus associés",compte:"Compte rendu",reports:"Rapports",reportform:"Nouveau rapport",email:"Créer un email",team:"Agenda de l'équipe",search:"Recherche d'interventions",folders:"Dossiers",clients:"Clients",locations:"Lieux",settings:"Paramètres"})[s]||"Omika"}
function activeNav(){const rootScreens=["home","team","search","folders","clients","locations","settings"],active=rootScreens.includes(state.screen)?state.screen:state.origin;$$("#sideNav [data-route],#mobileNav [data-route]").forEach(b=>{const yes=b.dataset.route===active;b.classList.toggle("active",yes);yes?b.setAttribute("aria-current","page"):b.removeAttribute("aria-current")})}
function showSync(){const txt=state.syncing?"Synchronisation…":`Synchronisé à ${fmt(state.lastSync,{hour:"2-digit",minute:"2-digit"})}`;id("railSyncTitle").textContent=state.syncing?"Synchronisation…":"Données à jour";id("railSyncTime").textContent=txt;["syncTop","syncMobile"].forEach(x=>{const b=id(x);if(b)b.disabled=state.syncing})}
function syncNow(){if(state.syncing)return;closeSheet();state.syncing=true;showSync();render();toast("Synchronisation lancée","Rendez-vous, interventions, rapports, notes et médias sont en cours de mise à jour.");setTimeout(()=>{state.syncing=false;state.lastSync=new Date();showSync();render();toast("Synchronisation terminée","Toutes les données Omika sont à jour.","ok")},1800)}
function avatar(person,size=""){return`<div class="avatar ${size}" style="background:${person.color||tone(person.name)}">${initials(person.name)}</div>`}
function empty(icon,title,text,action=""){return`<div class="empty"><div class="empty-icon">${icon}</div><h3>${esc(title)}</h3><p>${esc(text)}</p>${action}</div>`}
function phoneLabel(v){return String(v).replace(/(\d{2})(?=\d)/g,"$1 ")}
function appointmentTimeTotal(a){return a.timeEntries.reduce((n,x)=>n+duration(x.start,x.end),0)}
function appointmentRelationBadge(a){return a.interventionId?`<span class="badge primary mono">${a.interventionId}</span>`:`<span class="badge">Sans intervention</span>`}
/* =========================================================
COMPOSANTS : RENDEZ-VOUS ET INTERVENTIONS
========================================================= */
function apptCard(a,showCollab=false){const c=getClient(a.client),l=getLocation(a.location),p=getCollab(a.collaborator),s=statusInfo[a.status];return`<article class="appt ${s[2]}" data-open-appointment="${a.id}" tabindex="0" role="button">
<div class="appt-time"><strong>${a.from}</strong><span>${a.to}</span></div>
<div><div class="inline">${status(a.status)}<span class="badge mono">${a.id}</span>${appointmentRelationBadge(a)}</div><h3>${esc(c.name)} · ${esc(l.name)}</h3><p>${esc(a.description)}</p><div class="meta"><span>⌖ ${esc(l.address)}</span>${showCollab?`<span>◎ ${esc(p.name)}</span>`:""}<span>◷ ${humanDuration(duration(a.from,a.to))}</span></div></div>
<div class="appt-action"><button class="btn btn-soft btn-sm" data-open-appointment="${a.id}">Ouvrir le rendez-vous ›</button></div></article>`}
function interventionCard(i){const c=getClient(i.client),l=getLocation(i.location),apps=appointmentsFor(i),next=apps.find(a=>a.date>=iso(today));return`<article class="card result intervention-result"><div><div class="inline">${status(i.status)}<span class="badge mono">${i.id}</span><span class="badge">${apps.length} rendez-vous</span></div><h3>${esc(i.title)}</h3><p>${esc(c.name)} · ${esc(l.name)}<br>${esc(l.address)}</p><div class="meta"><span>◎ ${esc(getCollab(i.owner).name)}</span><span>${next?`▣ Prochain RDV : ${fmt(parse(next.date),{day:"numeric",month:"short"})} à ${next.from}`:"▣ Aucun rendez-vous planifié"}</span></div></div><button class="btn btn-soft" data-open-intervention="${i.id}">Ouvrir l'intervention ›</button></article>`}
/* =========================================================
ÉCRAN — MES RENDEZ-VOUS
========================================================= */
function homeScreen(){const mine=appointments.filter(a=>a.collaborator==="c1"),todayList=mine.filter(a=>a.date===iso(today)),done=todayList.filter(a=>a.status==="completed").length,week=mine.filter(a=>parse(a.date)>=weekStart(today)&&parse(a.date)<=add(weekStart(today),6));id("navCount").textContent=todayList.length;return`<section class="screen">
<div class="welcome"><div><span class="badge primary">● Mode connecté</span><h1>Bonjour Camille</h1><p>Votre agenda Omika couvre du ${fmt(rangeStart,{day:"numeric",month:"long"})} au ${fmt(rangeEnd,{day:"numeric",month:"long",year:"numeric"})}. Un rendez-vous peut être autonome ou lié à une intervention.</p><div class="actions"><button class="btn btn-primary" data-next-appointment>▶ Ouvrir le prochain rendez-vous</button><button class="btn btn-secondary" data-route="search">⌕ Rechercher une intervention</button></div></div>
<div class="summary-box"><span class="small muted">Progression de la journée</span><strong>${done} / ${todayList.length}</strong><span class="small muted">${todayList.filter(a=>a.interventionId).length} lié(s) · ${todayList.filter(a=>!a.interventionId).length} autonome(s)</span><div class="progressbar"><span style="width:${todayList.length?Math.round(done/todayList.length*100):0}%"></span></div></div></div>
<div class="grid-4" style="margin-bottom:18px"><div class="card stat"><div class="stat-icon">▣</div><div><strong>${todayList.length}</strong><span>rendez-vous aujourd'hui</span></div></div><div class="card stat"><div class="stat-icon">◷</div><div><strong>${humanDuration(todayList.reduce((n,a)=>n+appointmentTimeTotal(a),0))}</strong><span>temps saisi aujourd'hui</span></div></div><div class="card stat"><div class="stat-icon">✓</div><div><strong>${week.filter(a=>a.status==="completed").length}</strong><span>rendez-vous réalisés</span></div></div><div class="card stat"><div class="stat-icon">◇</div><div><strong>${todayList.filter(a=>!a.interventionId).length}</strong><span>sans intervention liée</span></div></div></div>
<div class="card agenda"><div class="agenda-tools"><div class="tool-group"><div class="segmented"><button data-agenda="day" class="${state.agendaView==="day"?"active":""}">Jour</button><button data-agenda="week" class="${state.agendaView==="week"?"active":""}">Semaine</button></div><button class="btn btn-secondary btn-sm" data-today>Aujourd'hui</button></div><div class="tool-group"><button class="icon-btn" data-period="-1">‹</button><div class="period">${agendaPeriod()}</div><button class="icon-btn" data-period="1">›</button><input type="date" id="jumpDate" min="${iso(rangeStart)}" max="${iso(rangeEnd)}" value="${iso(state.date)}" style="width:44px;padding:4px;color:transparent"></div></div>${dateStrip()}${agendaBody()}</div></section>`}
function agendaPeriod(){if(state.agendaView==="day")return(iso(state.date)===iso(today)?"Aujourd'hui · ":"")+fmt(state.date,{weekday:"long",day:"numeric",month:"long"});const a=weekStart(state.date),b=add(a,6);return`Du ${fmt(a,{day:"numeric",month:"short"})} au ${fmt(b,{day:"numeric",month:"short",year:"numeric"})}`}
function dateStrip(){const first=weekStart(state.date),mine=appointments.filter(a=>a.collaborator==="c1");return`<div class="date-strip">${Array.from({length:7},(_,i)=>add(first,i)).map(d=>{const n=mine.filter(a=>a.date===iso(d)).length,out=d<rangeStart||d>rangeEnd;return`<button class="date-chip ${iso(d)===iso(state.date)?"selected":""}" data-date="${iso(d)}" ${out?"disabled":""}><span>${fmt(d,{weekday:"short"}).replace(".","")}</span><strong>${d.getDate()}</strong><small>${n?n+" rdv":"—"}</small></button>`}).join("")}</div>`}
function agendaBody(){const mine=appointments.filter(a=>a.collaborator==="c1");if(state.agendaView==="day"){const list=mine.filter(x=>x.date===iso(state.date));return list.length?`<div class="list">${list.map(x=>apptCard(x)).join("")}</div>`:empty("▣","Aucun rendez-vous","Sélectionnez une autre date dans la période disponible.")}const first=weekStart(state.date);return Array.from({length:7},(_,i)=>add(first,i)).map(d=>{const list=mine.filter(x=>x.date===iso(d));return`<section class="day-group"><div class="day-title"><span>${fmt(d,{weekday:"long",day:"numeric",month:"long"})}</span><span>${list.length} rendez-vous</span></div>${list.length?`<div class="list">${list.map(x=>apptCard(x)).join("")}</div>`:`<div style="padding:14px 16px" class="small muted">Aucun rendez-vous planifié.</div>`}</section>`}).join("")}
/* =========================================================
ÉCRAN — DÉTAIL D'UN RENDEZ-VOUS
========================================================= */
function timeEntriesHTML(a){return a.timeEntries.length?a.timeEntries.map(x=>{const info=timeTypeInfo[x.type]||timeTypeInfo.other;return`<div class="row time-entry"><div class="row-icon">${info[1]}</div><div class="row-main"><strong>${esc(info[0])} · ${x.start}–${x.end}</strong><span>${humanDuration(duration(x.start,x.end))}${x.note?` · ${esc(x.note)}`:""}</span></div><button class="btn btn-ghost btn-sm" data-delete-time="${x.id}">Supprimer</button></div>`}).join(""):empty("◷","Aucun temps saisi","Ajoutez une plage avec son type, son heure de début et son heure de fin.")}
function appointmentScreen(a){const c=getClient(a.client),l=getLocation(a.location),p=getCollab(a.collaborator),i=a.interventionId?getIntervention(a.interventionId):null;return`<section class="screen"><div class="back"><button class="icon-btn" data-back>←</button><span class="small muted">Retour</span></div>
<div class="hero"><div class="hero-top"><div><span class="badge mono" style="background:rgba(255,255,255,.16);color:inherit">${a.id}</span><h1>${esc(a.title)}</h1><p>${esc(c.name)} · ${esc(l.name)} · ${fmt(parse(a.date),{weekday:"long",day:"numeric",month:"long",year:"numeric"})}, ${a.from}–${a.to}</p></div>${status(a.status)}</div><div class="hero-actions"><button class="btn" data-gps="${esc(l.address)}">⌖ GPS</button><button class="btn" data-phone="${c.contacts[0].phone}">☎ Appeler</button><button class="btn" data-email-address="${c.contacts[0].email}">✉ Email direct</button>${i?`<button class="btn main" data-open-intervention="${i.id}">⚒ Ouvrir l'intervention</button>`:""}</div></div>
<div class="detail-layout"><div class="stack-lg"><section class="card"><div class="card-head"><div><h2>Informations du rendez-vous</h2><p>Le rendez-vous reste un objet distinct de l'intervention</p></div><span>▣</span></div><div class="card-body stack-lg"><div class="grid-2"><div class="info"><span>Numéro de rendez-vous</span><strong class="mono">${a.id}</strong></div><div class="info"><span>Intervention liée</span><strong>${i?`<span class="mono">${i.id}</span>`:"Aucune"}</strong></div><div class="info"><span>Date</span><strong>${fmt(parse(a.date),{weekday:"long",day:"numeric",month:"long",year:"numeric"})}</strong></div><div class="info"><span>Créneau planifié</span><strong>${a.from}–${a.to} · ${humanDuration(duration(a.from,a.to))}</strong></div><div class="info"><span>Collaborateur</span><strong>${esc(p.name)}</strong></div><div class="info"><span>Temps saisi</span><strong>${humanDuration(appointmentTimeTotal(a))}</strong></div></div><div class="text-box"><h4>Description du rendez-vous</h4><p>${esc(a.description)}</p></div>${i?`<div class="relation-callout"><div><span class="badge primary">Intervention liée</span><h3>${esc(i.title)}</h3><p>${esc(i.short)}</p></div><button class="btn btn-primary" data-open-intervention="${i.id}">Ouvrir ${i.id}</button></div>`:`<div class="relation-callout neutral"><div><span class="badge">Rendez-vous autonome</span><h3>Aucune intervention associée</h3><p>Ce rendez-vous peut exister seul. Il ne sera pas retourné comme résultat dans la recherche d'interventions.</p></div></div>`}</div></section>
<section class="card"><div class="card-head"><div><h2>Saisie des temps</h2><p>Début et fin obligatoires, sans chronomètre</p></div><span class="badge primary">Total : ${humanDuration(appointmentTimeTotal(a))}</span></div><div class="card-body stack-lg"><div class="stack" id="timeEntries">${timeEntriesHTML(a)}</div><form id="timeForm" class="form-grid"><div class="field"><label class="required">Type de temps</label><select name="type" required>${Object.entries(timeTypeInfo).map(([k,v])=>`<option value="${k}">${v[0]}</option>`).join("")}</select></div><div class="field"><label class="required">Heure de début</label><input type="time" name="start" required value="${a.from}"></div><div class="field"><label class="required">Heure de fin</label><input type="time" name="end" required value="${a.to}"></div><div class="field span-2"><label>Commentaire</label><input name="note" placeholder="Ex. Trajet aller, intervention sur site…"></div><div class="actions" style="align-items:end"><button class="btn btn-primary">+ Ajouter le temps</button></div></form></div></section></div>
<aside class="detail-side"><section class="card" style="overflow:hidden"><div class="map"><div class="pin"><span>⌖</span></div></div><div class="card-body stack"><div><span class="badge primary">Lieu du rendez-vous</span><h2 style="margin:9px 0 4px;font-size:17px">${esc(l.name)}</h2><p class="small muted" style="margin:0;line-height:1.5">${esc(l.address)}</p></div><p class="small faint">Accès : ${esc(l.access)}</p><button class="btn btn-primary btn-block" data-gps="${esc(l.address)}">⌖ Lancer l'itinéraire</button><button class="btn btn-secondary btn-block" data-open-location="${l.id}">Voir la fiche du lieu</button></div></section><section class="card"><div class="card-head"><div><h2>Contacts</h2><p>Contacts associés au client</p></div><span>◎</span></div><div class="card-body stack">${c.contacts.map(x=>`<div class="row">${avatar({...x,color:tone(x.name)})}<div class="row-main"><strong>${esc(x.name)}</strong><span>${esc(x.role)} · ${phoneLabel(x.phone)}</span></div><button class="icon-btn" data-phone="${x.phone}">☎</button></div>`).join("")}</div></section></aside></div></section>`}
/* =========================================================
ÉCRAN — DÉTAIL D'UNE INTERVENTION
========================================================= */
function reportsOutputHTML(i){const rows=[];if(i.compteRendu)rows.push(`<div class="row"><div class="row-icon">✓</div><div class="row-main"><strong>Compte rendu fixe · ${esc(i.compteRendu.reference)}</strong><span>Validé le ${fmt(parse(i.compteRendu.createdAt),{day:"numeric",month:"long",year:"numeric"})}</span></div><button class="btn btn-soft btn-sm" data-open-compte="${i.id}">Ouvrir</button></div>`);rows.push(...i.reports.map(r=>`<div class="row"><div class="row-icon">▤</div><div class="row-main"><strong>${esc(r.title)}</strong><span>${r.id} · créé le ${fmt(parse(r.createdAt),{day:"numeric",month:"short",year:"numeric"})}</span></div><button class="btn btn-soft btn-sm" data-open-email-context="${i.id}|report|${r.id}">Email</button></div>`));return rows.length?rows.join(""):empty("▧","Aucun document de sortie","Créez le compte rendu fixe ou un rapport à partir d'un modèle.")}
function interventionScreen(i){const c=getClient(i.client),l=getLocation(i.location),p=getCollab(i.owner),apps=appointmentsFor(i);return`<section class="screen"><div class="back"><button class="icon-btn" data-back>←</button><span class="small muted">Retour</span></div>
<div class="hero"><div class="hero-top"><div><span class="badge mono" style="background:rgba(255,255,255,.16);color:inherit">${i.id}</span><h1>${esc(i.title)}</h1><p>${esc(c.name)} · ${esc(l.name)} · ${apps.length} rendez-vous lié${apps.length>1?"s":""}</p></div>${status(i.status)}</div><div class="hero-actions"><button class="btn" data-gps="${esc(l.address)}">⌖ GPS</button><button class="btn" data-phone="${c.contacts[0].phone}">☎ Appeler</button><button class="btn" data-open-email-context="${i.id}|direct|">✉ Email</button><button class="btn" data-open-reports="${i.id}">▤ Rapports</button><button class="btn main" data-open-compte="${i.id}">▧ Compte rendu fixe</button></div></div>
<div class="detail-layout"><div class="stack-lg"><section class="card"><div class="card-head"><div><h2>Informations de l'intervention</h2><p>Cette fiche reste indépendante des rendez-vous</p></div><span>⚒</span></div><div class="card-body stack-lg"><div class="grid-2"><div class="info"><span>N° d'intervention</span><strong class="mono">${i.id}</strong></div><div class="info"><span>Client</span><strong>${esc(c.name)}</strong></div><div class="info"><span>Lieu principal</span><strong>${esc(l.name)}</strong></div><div class="info"><span>Responsable</span><strong>${esc(p.name)}</strong></div><div class="info"><span>Priorité</span><strong>${esc(i.priority)}</strong></div><div class="info"><span>Ouverture</span><strong>${fmt(parse(i.opened),{day:"numeric",month:"long",year:"numeric"})}</strong></div></div><div class="text-box"><h4>Description de l'intervention</h4><p>${esc(i.detail)}</p></div><div class="text-box"><h4>Note de pilotage</h4><p>${esc(i.internalSummary)}</p></div></div></section>
<section class="card"><div class="card-head"><div><h2>Rendez-vous liés</h2><p>Une intervention peut avoir zéro, un ou plusieurs rendez-vous</p></div><button class="btn btn-primary btn-sm" data-add-appointment="${i.id}">+ Planifier</button></div>${apps.length?`<div class="list">${apps.map(a=>apptCard(a,true)).join("")}</div>`:empty("▣","Aucun rendez-vous lié","Cette intervention existe sans rendez-vous planifié.",`<button class="btn btn-primary" data-add-appointment="${i.id}">+ Planifier un rendez-vous</button>`)}</section>
<section class="card"><div class="card-head"><div><h2>Notes, images et documents</h2><p>Chaque contenu s'ouvre dans un écran dédié, adapté aux petits mobiles</p></div><span>◇</span></div><div class="card-body"><div class="resource-grid"><button class="resource-card" data-resource-kind="${i.id}|notes"><span class="resource-icon">✎</span><strong>Notes internes</strong><span>${i.notes.length} note${i.notes.length>1?"s":""}</span><b>Ouvrir ›</b></button><button class="resource-card" data-resource-kind="${i.id}|images"><span class="resource-icon">▧</span><strong>Images</strong><span>${i.images.length} image${i.images.length>1?"s":""}</span><b>Ouvrir ›</b></button><button class="resource-card" data-resource-kind="${i.id}|documents"><span class="resource-icon">▤</span><strong>Documents</strong><span>${i.documents.length} document${i.documents.length>1?"s":""}</span><b>Ouvrir ›</b></button></div></div></section>
<section class="card"><div class="card-head"><div><h2>Compte rendu et rapports créés</h2><p>Le compte rendu est fixe ; les rapports utilisent des modèles</p></div><div class="actions"><button class="btn btn-secondary btn-sm" data-open-reports="${i.id}">Voir les modèles</button></div></div><div class="card-body stack">${reportsOutputHTML(i)}</div></section></div>
<aside class="detail-side"><section class="card" style="overflow:hidden"><div class="map"><div class="pin"><span>⌖</span></div></div><div class="card-body stack"><div><span class="badge primary">Lieu principal</span><h2 style="margin:9px 0 4px;font-size:17px">${esc(l.name)}</h2><p class="small muted" style="margin:0;line-height:1.5">${esc(l.address)}</p></div><p class="small faint">Accès : ${esc(l.access)}</p><button class="btn btn-primary btn-block" data-gps="${esc(l.address)}">⌖ Lancer l'itinéraire</button><button class="btn btn-secondary btn-block" data-open-location="${l.id}">Voir la fiche du lieu</button></div></section><section class="card"><div class="card-head"><div><h2>Contacts</h2><p>Rôles dans l'intervention</p></div><span>◎</span></div><div class="card-body stack">${c.contacts.map(x=>`<div class="row">${avatar({...x,color:tone(x.name)})}<div class="row-main"><strong>${esc(x.name)}</strong><span>${esc(x.role)} · ${phoneLabel(x.phone)}</span></div><button class="icon-btn" data-phone="${x.phone}">☎</button></div>`).join("")}</div></section></aside></div></section>`}
/* =========================================================
ÉCRAN — NOTES, IMAGES OU DOCUMENTS D'UNE INTERVENTION
========================================================= */
function notesHTML(i){return i.notes.length?i.notes.map(n=>`<div class="row"><div class="row-icon">✎</div><div class="row-main"><strong>${esc(n.author)} · ${fmt(parse(n.date),{day:"numeric",month:"short",year:"numeric"})}</strong><span>${esc(n.text)}</span></div></div>`).join(""):empty("✎","Aucune note","Ajoutez une note interne à cette intervention.")}
function photosHTML(i){return i.images.length?i.images.map(x=>`<article class="photo"><img src="${x.url}" alt="${esc(x.name)}"><div class="photo-body"><div class="cluster"><strong>${esc(x.name)}</strong><button class="btn btn-ghost btn-sm" data-delete-image="${x.id}">Supprimer</button></div><input data-photo-note="${x.id}" value="${esc(x.note)}" placeholder="Ajouter une note"><button class="btn btn-secondary btn-sm" data-save-photo="${x.id}">✓ Enregistrer la note</button></div></article>`).join(""):empty("▧","Aucune image","Ajoutez une photo de l'équipement, d'une anomalie ou d'une pièce remplacée.")}
function documentsHTML(i){return i.documents.length?i.documents.map(d=>`<div class="row"><div class="row-icon">▤</div><div class="row-main"><strong>${esc(d[0])}</strong><span>${esc(d[1])}</span></div><button class="icon-btn" data-document="${esc(d[0])}">◉</button></div>`).join(""):empty("▤","Aucun document","Ajoutez un document associé à cette intervention.")}
function resourceScreen(i,kind){const labels={notes:["Notes internes","✎"],images:["Images associées","▧"],documents:["Documents associés","▤"]},label=labels[kind]||labels.notes;let content="";if(kind==="notes")content=`<div class="stack-lg"><div class="stack" id="resourceNotes">${notesHTML(i)}</div><form id="resourceNoteForm" class="stack"><div class="field"><label>Ajouter une note interne</label><textarea name="note" required placeholder="Observation, consigne, pièce à commander…"></textarea></div><div><button class="btn btn-primary">+ Ajouter la note</button></div></form></div>`;if(kind==="images")content=`<div class="stack-lg"><div class="photos" id="resourcePhotos">${photosHTML(i)}</div><label class="drop" for="resourceImageInput"><strong>▧ Ajouter des images</strong><span class="small">Touchez ici ou déposez des fichiers JPG/PNG</span></label><input class="hidden" type="file" id="resourceImageInput" accept="image/*" multiple></div>`;if(kind==="documents")content=`<div class="stack-lg"><div class="stack" id="resourceDocuments">${documentsHTML(i)}</div><label class="drop" for="resourceDocumentInput"><strong>▤ Ajouter des documents</strong><span class="small">PDF, DOCX, XLSX ou autre fichier de démonstration</span></label><input class="hidden" type="file" id="resourceDocumentInput" multiple></div>`;return`<section class="screen"><div class="back"><button class="icon-btn" data-back>←</button><span class="small muted">Retour à l'intervention</span></div><div class="page-head"><div><span class="badge primary mono">${i.id}</span><h1>${label[0]}</h1><p>${esc(i.title)}</p></div></div><section class="card"><div class="tabs resource-nav"><button class="tab ${kind==="notes"?"active":""}" data-resource-kind="${i.id}|notes">Notes</button><button class="tab ${kind==="images"?"active":""}" data-resource-kind="${i.id}|images">Images</button><button class="tab ${kind==="documents"?"active":""}" data-resource-kind="${i.id}|documents">Documents</button></div><div class="card-body">${content}</div></section></section>`}
/* =========================================================
ÉCRAN — COMPTE RENDU FIXE AVEC DEUX SIGNATURES OBLIGATOIRES
========================================================= */
function compteRenduScreen(i){const c=getClient(i.client),l=getLocation(i.location),p=getCollab(i.owner),apps=appointmentsFor(i),latest=apps.at(-1);return`<section class="screen"><div class="back"><button class="icon-btn" data-back>←</button><span class="small muted">Retour à l'intervention</span></div><div class="page-head"><div><span class="badge primary">▧ Formulaire fixe Omika</span><h1>Compte rendu d'intervention</h1><p>${i.id} · ${esc(c.name)} · structure non configurable</p></div></div>
<form id="compteRenduForm"><div class="report-layout"><div class="stack-lg"><section class="card fixed-document"><div class="fixed-banner"><div><strong>COMPTE RENDU D'INTERVENTION</strong><span>Référence de formulaire : OMIKA-CR-01</span></div><span class="badge">Formulaire fixe</span></div><div class="dynamic"><div class="paper-meta"><div><span>Intervention</span><strong class="mono">${i.id}</strong></div><div><span>Client</span><strong>${esc(c.name)}</strong></div><div><span>Lieu</span><strong>${esc(l.name)}</strong></div><div><span>Technicien</span><strong>${esc(p.name)}</strong></div></div>
<div class="form-section"><div class="form-section-title"><span>1</span><div><strong>Informations générales</strong><small>Cadre fixe du compte rendu</small></div></div><div class="form-grid"><div class="field"><label class="required">Date d'intervention</label><input type="date" name="date" required value="${latest?.date||iso(today)}"></div><div class="field"><label class="required">Contact client présent</label><input name="clientContact" required placeholder="Nom et fonction"></div><div class="field"><label class="required">Heure d'arrivée</label><input type="time" name="arrival" required value="${latest?.from||"08:00"}"></div><div class="field"><label class="required">Heure de départ</label><input type="time" name="departure" required value="${latest?.to||"10:00"}"></div><div class="field span-2"><label class="required">Équipement ou zone concernée</label><input name="equipment" required placeholder="Ex. Groupe froid GF-02"></div></div></div>
<div class="form-section"><div class="form-section-title"><span>2</span><div><strong>Demande et travaux réalisés</strong><small>Champs toujours présents</small></div></div><div class="form-grid"><div class="field span-2"><label class="required">Demande initiale / anomalie signalée</label><textarea name="request" required placeholder="Décrivez la demande du client…"></textarea></div><div class="field span-2"><label class="required">Travaux et opérations réalisés</label><textarea name="work" required placeholder="Décrivez précisément les actions menées…"></textarea></div><div class="field"><label>Pièces ou consommables utilisés</label><input name="parts" placeholder="Références ou « Aucune »"></div><div class="field"><label class="required">Résultat de l'intervention</label><select name="result" required><option value="">Sélectionner…</option><option>Résolu</option><option>Résolu avec réserve</option><option>Non résolu</option><option>Intervention à poursuivre</option></select></div></div></div>
<div class="form-section"><div class="form-section-title"><span>3</span><div><strong>Contrôles et recommandations</strong><small>Bloc fixe de validation technique</small></div></div><div class="checks" style="margin-bottom:14px"><label class="check"><input type="checkbox" name="controls" value="Essai fonctionnel"><span>Essai fonctionnel réalisé</span></label><label class="check"><input type="checkbox" name="controls" value="Sécurités"><span>Sécurités contrôlées</span></label><label class="check"><input type="checkbox" name="controls" value="Nettoyage"><span>Zone nettoyée</span></label><label class="check"><input type="checkbox" name="controls" value="Explications"><span>Explications données au client</span></label></div><div class="form-grid"><div class="field span-2"><label>Recommandations et suite à prévoir</label><textarea name="recommendations" placeholder="Préconisations, réserves, nouvelle intervention…"></textarea></div><div class="field span-2"><label>Observations du client</label><textarea name="clientComments" placeholder="Commentaires éventuels du client…"></textarea></div><label class="check span-2"><input type="checkbox" name="followup"><span>Une intervention complémentaire doit être planifiée.</span></label></div></div></div></section>
<section class="card dynamic"><h2>Signatures obligatoires</h2><p>Le compte rendu ne peut pas être validé sans la signature du technicien et celle du client.</p><div class="signatures"><div class="signature"><div class="signature-head"><span>Signature technicien *</span><button type="button" class="btn btn-ghost btn-sm" data-clear-sign="tech">Effacer</button></div><canvas id="crSignTech" width="720" height="240"></canvas></div><div class="signature"><div class="signature-head"><span>Signature client *</span><button type="button" class="btn btn-ghost btn-sm" data-clear-sign="client">Effacer</button></div><canvas id="crSignClient" width="720" height="240"></canvas></div></div></section></div>
<aside class="card report-side"><div class="card-head"><div><h2>Validation</h2><p>Compte rendu fixe</p></div><span>◉</span></div><div class="card-body stack-lg"><div class="stack"><div class="cluster"><span class="small faint">Intervention</span><strong class="small mono">${i.id}</strong></div><div class="cluster"><span class="small faint">Client</span><strong class="small">${esc(c.name)}</strong></div><div class="cluster"><span class="small faint">Lieu</span><strong class="small">${esc(l.name)}</strong></div><div class="cluster"><span class="small faint">Statut actuel</span><strong class="small">${i.compteRendu?esc(i.compteRendu.status):"Non créé"}</strong></div></div><label class="check"><input type="checkbox" name="confirm" required><span>Je confirme l'exactitude des informations saisies.</span></label><button type="button" class="btn btn-secondary btn-block" data-save-compte>Enregistrer le brouillon</button><button class="btn btn-primary btn-block">✓ Valider le compte rendu</button></div></aside></div></form></section>`}
/* =========================================================
ÉCRANS — MODÈLES DE RAPPORTS ET FORMULAIRE DYNAMIQUE
========================================================= */
function dynamicField(f){const req=f.required?"required":"",cl=f.span?"span-2":"",value=f.type==="date"?iso(today):"";if(["text","date","time","number"].includes(f.type))return`<div class="field ${cl}"><label class="${req}">${esc(f.label)}</label><input type="${f.type}" name="${f.name}" ${req} value="${value}" placeholder="${esc(f.placeholder||"")}"></div>`;if(f.type==="select")return`<div class="field ${cl}"><label class="${req}">${esc(f.label)}</label><select name="${f.name}" ${req}><option value="">Sélectionner…</option>${f.options.map(x=>`<option>${esc(x)}</option>`).join("")}</select></div>`;if(f.type==="textarea")return`<div class="field ${cl}"><label class="${req}">${esc(f.label)}</label><textarea name="${f.name}" ${req} placeholder="${esc(f.placeholder||"")}"></textarea></div>`;if(f.type==="checks")return`<fieldset class="${cl}" style="border:0;padding:0;margin:0"><legend class="field-title" style="margin-bottom:8px">${esc(f.label)}</legend><div class="checks">${f.options.map(x=>`<label class="check"><input type="checkbox" name="${f.name}" value="${esc(x)}"><span>${esc(x)}</span></label>`).join("")}</div></fieldset>`;return""}
function reportsScreen(i){return`<section class="screen"><div class="back"><button class="icon-btn" data-back>←</button><span class="small muted">Retour à l'intervention</span></div><div class="page-head"><div><span class="badge primary mono">${i.id}</span><h1>Rapports</h1><p>Choisissez un modèle, remplissez son formulaire, puis envoyez le rapport créé par email.</p></div></div><div class="stack-lg">${i.reports.length?`<section class="card"><div class="card-head"><div><h2>Rapports créés</h2><p>${i.reports.length} document${i.reports.length>1?"s":""} disponible${i.reports.length>1?"s":""}</p></div><span>▤</span></div><div class="card-body stack">${i.reports.map(r=>`<div class="row"><div class="row-icon">▤</div><div class="row-main"><strong>${esc(r.title)}</strong><span>${r.id} · ${fmt(parse(r.createdAt),{day:"numeric",month:"long",year:"numeric"})}</span></div><button class="btn btn-primary btn-sm" data-open-email-context="${i.id}|report|${r.id}">✉ Envoyer</button></div>`).join("")}</div></section>`:""}<section class="card"><div class="card-head"><div><h2>Modèles de rapports</h2><p>Chaque modèle génère un formulaire différent</p></div><span class="badge">${reportTemplates.length} modèles</span></div><div class="card-body"><div class="template-grid">${reportTemplates.map(t=>`<article class="template-card"><div class="template-icon">${t.icon}</div><div><span class="badge primary">Modèle</span><h3>${esc(t.name)}</h3><p>${esc(t.description)}</p></div><button class="btn btn-soft btn-block" data-report-template="${i.id}|${t.id}">Utiliser ce modèle ›</button></article>`).join("")}</div></div></section></div></section>`}
function reportFormScreen(i,t){const c=getClient(i.client),l=getLocation(i.location);return`<section class="screen"><div class="back"><button class="icon-btn" data-back>←</button><span class="small muted">Retour aux modèles</span></div><div class="page-head"><div><span class="badge primary">${t.icon} Modèle de rapport</span><h1>${esc(t.name)}</h1><p>${i.id} · ${esc(c.name)} · formulaire généré à partir du modèle sélectionné</p></div></div><form id="dynamicReportForm" data-template="${t.id}"><div class="report-layout"><section class="card dynamic"><h2>Formulaire du modèle</h2><p>${esc(t.description)}</p><div class="form-grid">${t.fields.map(dynamicField).join("")}</div></section><aside class="card report-side"><div class="card-head"><div><h2>Créer le rapport</h2><p>Le rapport pourra ensuite être envoyé par email</p></div><span>▤</span></div><div class="card-body stack-lg"><div class="stack"><div class="cluster"><span class="small faint">Intervention</span><strong class="small mono">${i.id}</strong></div><div class="cluster"><span class="small faint">Client</span><strong class="small">${esc(c.name)}</strong></div><div class="cluster"><span class="small faint">Lieu</span><strong class="small">${esc(l.name)}</strong></div><div class="cluster"><span class="small faint">Modèle</span><strong class="small">${esc(t.name)}</strong></div></div><button type="button" class="btn btn-secondary btn-block" data-save-dynamic-report>Enregistrer le brouillon</button><button class="btn btn-primary btn-block">✓ Créer le rapport</button></div></aside></div></form></section>`}
/* =========================================================
ÉCRAN — EMAIL DIRECT OU CRÉÉ DEPUIS UN MODÈLE
========================================================= */
function emailField(f,defaults={}){const type=f.type||"text",v=defaults[f.name]||"";if(type==="textarea")return`<div class="field span-2"><label class="${f.required?"required":""}">${esc(f.label)}</label><textarea name="${f.name}" ${f.required?"required":""} placeholder="${esc(f.placeholder||"")}">${esc(v)}</textarea></div>`;return`<div class="field"><label class="${f.required?"required":""}">${esc(f.label)}</label><input type="${type}" name="${f.name}" ${f.required?"required":""} value="${esc(v)}" placeholder="${esc(f.placeholder||"")}"></div>`}
function emailDefaults(i,mode,report){const c=getClient(i.client),apps=appointmentsFor(i),next=apps.find(a=>a.date>=iso(today));return{contactName:c.contacts[0]?.name||"",message:`Ce rapport présente les opérations réalisées et les recommandations associées à l'intervention ${i.id}.`,replyDate:iso(add(today,7)),appointmentDate:next?.date||iso(add(today,3)),appointmentTime:next?.from||"09:00",accessInfo:getLocation(i.location).access,requestedItems:"Merci de nous transmettre les plans à jour, les références des équipements et les derniers relevés disponibles.",deadline:iso(add(today,5)),reportId:report?.id||""}}
function selectedAttachments(i,reportId){const items=[];if(reportId){const r=i.reports.find(x=>x.id===reportId);if(r)items.push({id:r.id,label:`${r.title} · ${r.id}`,checked:true})}i.reports.filter(x=>x.id!==reportId).forEach(r=>items.push({id:r.id,label:`${r.title} · ${r.id}`,checked:false}));if(i.compteRendu)items.push({id:i.compteRendu.reference,label:`Compte rendu · ${i.compteRendu.reference}`,checked:false});i.documents.forEach((d,index)=>items.push({id:`doc-${index}`,label:d[0],checked:false}));return items}
function emailScreen(i,mode,reportId){const c=getClient(i.client),l=getLocation(i.location),report=i.reports.find(r=>r.id===reportId),template=emailTemplates.find(t=>t.id===mode),defaults=emailDefaults(i,mode,report),draft=template?template.build(i,defaults,report):null,attachments=selectedAttachments(i,reportId);return`<section class="screen"><div class="back"><button class="icon-btn" data-back>←</button><span class="small muted">Retour à l'intervention</span></div><div class="page-head"><div><span class="badge primary mono">${i.id}</span><h1>Créer un email</h1><p>Email libre ou email construit à partir d'un modèle Omika.</p></div></div><div class="email-layout"><section class="card"><div class="card-head"><div><h2>Mode de création</h2><p>Sélectionnez l'approche souhaitée</p></div><span>✉</span></div><div class="card-body"><div class="email-mode-grid"><button class="email-mode ${mode==="direct"?"active":""}" data-email-mode="${i.id}|direct|${reportId||""}"><span>✎</span><strong>Email libre</strong><small>Objet et corps saisis directement</small></button>${emailTemplates.map(t=>`<button class="email-mode ${mode===t.id?"active":""}" data-email-mode="${i.id}|${t.id}|${reportId||""}"><span>▤</span><strong>${esc(t.name)}</strong><small>${esc(t.description)}</small></button>`).join("")}</div></div></section><form class="card" id="emailForm" data-mode="${mode}" data-report="${reportId||""}"><div class="card-head"><div><h2>${mode==="direct"?"Email direct":esc(template?.name||"Modèle d'email")}</h2><p>${mode==="direct"?"Saisissez directement le message":"Complétez les paramètres puis vérifiez l'aperçu généré"}</p></div><span class="badge primary">${mode==="direct"?"Libre":"Modèle"}</span></div><div class="card-body stack-lg"><div class="form-grid"><div class="field"><label class="required">Destinataire</label><input type="email" name="to" required value="${esc(c.contacts[0]?.email||c.email)}"></div><div class="field"><label>Copie</label><input type="email" name="cc" value="${esc(c.email)}"></div>${mode==="direct"?`<div class="field span-2"><label class="required">Objet</label><input name="subject" required value="Intervention ${i.id} — ${esc(i.title)}"></div><div class="field span-2"><label class="required">Corps de l'email</label><textarea name="body" required>Bonjour ${esc(c.contacts[0]?.name||"")},\n\nJe vous contacte au sujet de l'intervention ${i.id} sur le site ${esc(l.name)}.\n\nCordialement,\nCamille Bernard\nOmika</textarea></div>`:template.fields.map(f=>emailField(f,defaults)).join("")}</div>${mode!=="direct"?`<div class="email-preview"><span class="badge">Aperçu généré</span><div class="field"><label>Objet construit</label><input id="emailSubjectPreview" readonly value="${esc(draft.subject)}"></div><div class="field"><label>Corps construit</label><textarea id="emailBodyPreview" readonly>${esc(draft.body)}</textarea></div></div>`:""}<fieldset style="border:0;padding:0;margin:0"><legend class="field-title" style="margin-bottom:9px">Pièces jointes simulées</legend><div class="checks">${attachments.length?attachments.map(x=>`<label class="check"><input type="checkbox" name="attachments" value="${esc(x.id)}" ${x.checked?"checked":""}><span>${esc(x.label)}</span></label>`).join(""):`<span class="small muted">Aucune pièce jointe disponible.</span>`}</div></fieldset><div class="relation-callout"><div><h3>Email prêt à être créé</h3><p>Le bouton ouvre le client de messagerie configuré sur l'appareil avec l'objet et le corps générés.</p></div><button class="btn btn-primary">✉ Créer l'email</button></div></div></form></div></section>`}
/* =========================================================
ÉCRAN — AGENDA DES AUTRES COLLABORATEURS
========================================================= */
function teamFiltered(){const end=add(state.teamWeek,6);return appointments.filter(a=>a.collaborator!=="c1"&&parse(a.date)>=state.teamWeek&&parse(a.date)<=end&&(state.teamCollab==="all"||a.collaborator===state.teamCollab))}
function teamScreen(){const end=add(state.teamWeek,6),items=teamFiltered();return`<section class="screen"><div class="page-head"><div><h1>Agenda de l'équipe</h1><p>Consultez les rendez-vous des autres collaborateurs. L'ouverture affiche toujours le rendez-vous, puis son éventuelle intervention liée.</p></div><button class="btn btn-secondary" data-team-today>▣ Cette semaine</button></div><div class="collabs"><button class="collab ${state.teamCollab==="all"?"active":""}" data-collab="all"><div class="avatar" style="width:29px;height:29px;background:var(--primary)">+</div>Toute l'équipe</button>${collaborators.slice(1).map(p=>`<button class="collab ${state.teamCollab===p.id?"active":""}" data-collab="${p.id}">${avatar(p)}${esc(p.name)}</button>`).join("")}</div><div class="card"><div class="agenda-tools"><div class="tool-group"><button class="icon-btn" data-team-week="-1">‹</button><strong class="small">Du ${fmt(state.teamWeek,{day:"numeric",month:"short"})} au ${fmt(end,{day:"numeric",month:"short",year:"numeric"})}</strong><button class="icon-btn" data-team-week="1">›</button></div><div class="segmented"><button data-team-view="list" class="${state.teamView==="list"?"active":""}">Liste</button><button data-team-view="calendar" class="${state.teamView==="calendar"?"active":""}">Calendrier</button></div></div>${state.teamView==="list"?(items.length?`<div class="list">${items.map(a=>apptCard(a,true)).join("")}</div>`:empty("◎","Aucun rendez-vous","Aucun rendez-vous ne correspond au collaborateur et à la semaine.")):`<div class="team-calendar-wrap"><div class="team-calendar">${Array.from({length:7},(_,i)=>add(state.teamWeek,i)).map(d=>{const list=items.filter(a=>a.date===iso(d));return`<div class="team-day"><div class="team-day-head"><strong>${fmt(d,{weekday:"long"})}</strong><span>${fmt(d,{day:"numeric",month:"short"})} · ${list.length} rdv</span></div>${list.map(a=>{const c=getClient(a.client),p=getCollab(a.collaborator);return`<div class="mini-event" data-open-appointment="${a.id}" style="border-left-color:${p.color}"><strong>${a.from} · ${esc(c.name)}</strong><span>${esc(p.name)}</span><span>${a.interventionId?esc(a.interventionId):"Sans intervention"}</span></div>`}).join("")||`<div class="small faint" style="padding:8px">Libre</div>`}</div>`}).join("")}</div></div>`}</div></section>`}
/* =========================================================
ÉCRAN — RECHERCHE UNIQUEMENT DANS LES INTERVENTIONS
========================================================= */
function searchValues(){return{number:id("qNumber")?.value||"",client:id("qClient")?.value||"",place:id("qPlace")?.value||"",relation:id("qRelation")?.value||"all",date:id("qDate")?.value||"",collab:id("qCollab")?.value||""}}
function searchResults(q=state.search||{}){const has=Object.entries(q).some(([k,v])=>k!=="relation"&&Boolean(v))||(q.relation&&q.relation!=="all");return interventions.filter(i=>{const c=getClient(i.client),l=getLocation(i.location),apps=appointmentsFor(i),withAppointments=apps.length>0;return(!q.number||norm(i.id).includes(norm(q.number)))&&(!q.client||norm(`${c.name} ${c.ref}`).includes(norm(q.client)))&&(!q.place||norm(`${l.name} ${l.address}`).includes(norm(q.place)))&&(!q.relation||q.relation==="all"||(q.relation==="with"&&withAppointments)||(q.relation==="without"&&!withAppointments))&&(!q.date||apps.some(a=>a.date===q.date))&&(!q.collab||i.owner===q.collab||apps.some(a=>a.collaborator===q.collab))}).sort((a,b)=>b.opened.localeCompare(a.opened)).slice(0,has?40:10)}
function searchScreen(){const q=state.search||{relation:"all"},items=searchResults(q);return`<section class="screen"><div class="page-head"><div><h1>Recherche d'interventions</h1><p>La recherche retourne uniquement des interventions. Les critères de rendez-vous servent seulement à filtrer les interventions qui en possèdent.</p></div></div><div class="relation-callout" style="margin-bottom:16px"><div><span class="badge primary">Règle métier</span><h3>Un résultat reste toujours une intervention</h3><p>Les rendez-vous autonomes n'apparaissent pas ici. La liste des rendez-vous liés est disponible dans chaque fiche intervention.</p></div></div><form class="card search-form" id="searchForm"><div class="grid-3"><div class="field"><label>N° d'intervention</label><input id="qNumber" value="${esc(q.number||"")}" placeholder="Ex. INT-${today.getFullYear()}-1104"></div><div class="field"><label>Client</label><input id="qClient" value="${esc(q.client||"")}" placeholder="Nom ou référence"></div><div class="field"><label>Lieu ou adresse</label><input id="qPlace" value="${esc(q.place||"")}" placeholder="Site, ville, rue…"></div><div class="field"><label>Relation aux rendez-vous</label><select id="qRelation"><option value="all" ${q.relation==="all"?"selected":""}>Toutes les interventions</option><option value="with" ${q.relation==="with"?"selected":""}>Avec au moins un rendez-vous</option><option value="without" ${q.relation==="without"?"selected":""}>Sans rendez-vous</option></select></div><div class="field"><label>Date d'un rendez-vous lié</label><input id="qDate" type="date" value="${esc(q.date||"")}"></div><div class="field"><label>Collaborateur</label><select id="qCollab"><option value="">Tous les collaborateurs</option>${collaborators.map(p=>`<option value="${p.id}" ${q.collab===p.id?"selected":""}>${esc(p.name)}</option>`).join("")}</select></div><div class="actions span-2" style="align-items:end"><button type="button" class="btn btn-secondary" data-reset-search>Réinitialiser</button><button class="btn btn-primary">⌕ Rechercher les interventions</button></div></div></form><div class="cluster" style="margin-bottom:10px"><h2 style="margin:0;font-size:16px">${Object.values(q).some(v=>v&&v!=="all")?"Résultats de recherche":"Interventions récentes"}</h2><span class="badge">${items.length} résultat${items.length>1?"s":""}</span></div><div class="stack">${items.length?items.map(interventionCard).join(""):empty("⌕","Aucune intervention","Modifiez les critères de recherche.")}</div></section>`}
/* =========================================================
ÉCRAN — DOSSIERS
========================================================= */
function folderScreen(){const f=folders.find(x=>x.id===state.folder)||folders[0],c=getClient(f.client),locs=f.locations.map(getLocation),ints=f.interventions.map(getIntervention).filter(Boolean),apps=appointments.filter(a=>a.interventionId&&f.interventions.includes(a.interventionId));return`<section class="screen"><div class="page-head"><div><h1>Dossiers</h1><p>Un dossier regroupe des interventions. Les rendez-vous affichés sont ceux liés à ces interventions.</p></div><button class="btn btn-primary" data-add-intervention="${f.id}">+ Nouvelle intervention</button></div><div class="master-detail"><aside class="card master"><div class="card-head"><div><h2>Dossiers actifs</h2><p>${folders.length} dossiers</p></div><span>▤</span></div><div class="master-list">${folders.map(x=>`<button class="master-item ${x.id===f.id?"active":""}" data-folder="${x.id}"><strong>${esc(x.title)}</strong><span>${esc(getClient(x.client).name)} · ${esc(x.priority)}</span><small class="mono">${x.id} · échéance ${fmt(parse(x.due),{day:"numeric",month:"short"})}</small></button>`).join("")}</div></aside><article class="card"><div class="entity-head"><div class="entity-title"><div class="entity-icon">▤</div><div><span class="badge primary mono">${f.id}</span><h1>${esc(f.title)}</h1><p>${esc(c.name)} · Pilote : ${esc(f.manager)}</p></div></div><button class="btn btn-primary" data-add-intervention="${f.id}">+ Ajouter une intervention</button></div><div class="tabs"><button class="tab active" data-tab="overview">Vue d'ensemble</button><button class="tab" data-tab="interventions">Interventions</button><button class="tab" data-tab="appointments">Rendez-vous liés</button><button class="tab" data-tab="documents">Documents</button></div><div class="tab-panel active" data-panel="overview"><div class="stack-lg"><div class="grid-2"><div class="info"><span>Statut</span><strong>${esc(f.status)}</strong></div><div class="info"><span>Priorité</span><strong>${esc(f.priority)}</strong></div><div class="info"><span>Interventions</span><strong>${ints.length}</strong></div><div class="info"><span>Rendez-vous liés</span><strong>${apps.length}</strong></div></div><div class="text-box"><h4>Résumé du dossier</h4><p>${esc(f.summary)}</p></div><div class="grid-2"><div class="info"><span>Client concerné</span><strong>${esc(c.name)}</strong><button class="btn btn-ghost btn-sm" data-open-client="${c.id}">Voir la fiche</button></div><div class="info"><span>Lieux concernés</span><strong>${locs.map(x=>esc(x.name)).join("<br>")}</strong></div></div></div></div><div class="tab-panel" data-panel="interventions"><div class="stack">${ints.length?ints.map(interventionCard).join(""):empty("⚒","Aucune intervention","Ajoutez une intervention au dossier.")}</div></div><div class="tab-panel" data-panel="appointments"><div class="stack">${apps.length?apps.map(a=>apptCard(a,true)).join(""):empty("▣","Aucun rendez-vous lié","Les interventions de ce dossier n'ont aucun rendez-vous.")}</div></div><div class="tab-panel" data-panel="documents"><div class="stack">${f.docs.map(x=>`<div class="row"><div class="row-icon">▤</div><div class="row-main"><strong>${esc(x)}</strong><span>Document du dossier</span></div><button class="icon-btn" data-document="${esc(x)}">◉</button></div>`).join("")}</div></div></article></div></section>`}
/* =========================================================
ÉCRANS — FICHES CLIENTS ET LIEUX
========================================================= */
function clientScreen(){const c=getClient(state.client)||clients[0],locs=locations.filter(l=>l.client===c.id),ints=interventions.filter(i=>i.client===c.id).slice().reverse(),apps=appointments.filter(a=>a.client===c.id).slice().reverse().slice(0,15);return`<section class="screen"><div class="page-head"><div><h1>Fiches clients</h1><p>Les interventions et les rendez-vous sont présentés dans deux onglets distincts.</p></div></div><div class="master-detail"><aside class="card master"><div class="card-head"><div><h2>Clients</h2><p>${clients.length} clients</p></div><span>▥</span></div><div class="master-list">${clients.map(x=>`<button class="master-item ${x.id===c.id?"active":""}" data-client="${x.id}"><strong>${esc(x.name)}</strong><span>${esc(x.sector)}</span><small>${x.ref} · ${locations.filter(l=>l.client===x.id).length} lieu(x)</small></button>`).join("")}</div></aside><article class="card"><div class="entity-head"><div class="entity-title"><div class="entity-icon">▥</div><div><span class="badge primary">Client actif</span><h1>${esc(c.name)}</h1><p>${esc(c.legal)} · ${c.ref}</p></div></div><div class="actions"><button class="btn btn-secondary" data-phone="${c.phone}">☎ Appeler</button><button class="btn btn-primary" data-email-address="${c.email}">✉ Email</button></div></div><div class="tabs"><button class="tab active" data-tab="overview">Informations</button><button class="tab" data-tab="contacts">Contacts</button><button class="tab" data-tab="locations">Lieux</button><button class="tab" data-tab="interventions">Interventions</button><button class="tab" data-tab="appointments">Rendez-vous</button><button class="tab" data-tab="documents">Documents</button></div><div class="tab-panel active" data-panel="overview"><div class="stack-lg"><div class="grid-2"><div class="info"><span>Secteur</span><strong>${esc(c.sector)}</strong></div><div class="info"><span>Téléphone</span><strong>${esc(c.phone)}</strong></div><div class="info"><span>Email</span><strong>${esc(c.email)}</strong></div><div class="info"><span>Adresse</span><strong>${esc(c.address)}</strong></div></div><div class="text-box"><h4>Notes client</h4><p>${esc(c.note)}</p></div></div></div><div class="tab-panel" data-panel="contacts"><div class="stack">${c.contacts.map(x=>`<div class="row">${avatar({...x,color:tone(x.name)})}<div class="row-main"><strong>${esc(x.name)}</strong><span>${esc(x.role)} · ${esc(x.phone)}<br>${esc(x.email)}</span></div><button class="icon-btn" data-phone="${x.phone}">☎</button></div>`).join("")}</div></div><div class="tab-panel" data-panel="locations"><div class="stack">${locs.map(l=>`<div class="row"><div class="row-icon">⌖</div><div class="row-main"><strong>${esc(l.name)}</strong><span>${esc(l.address)}</span></div><button class="btn btn-soft btn-sm" data-open-location="${l.id}">Ouvrir</button></div>`).join("")}</div></div><div class="tab-panel" data-panel="interventions"><div class="stack">${ints.map(interventionCard).join("")||empty("⚒","Aucune intervention","Aucune intervention n'est associée à ce client.")}</div></div><div class="tab-panel" data-panel="appointments"><div class="stack">${apps.map(a=>apptCard(a,true)).join("")||empty("▣","Aucun rendez-vous","Aucun rendez-vous n'est associé à ce client.")}</div></div><div class="tab-panel" data-panel="documents"><div class="stack">${c.docs.map(x=>`<div class="row"><div class="row-icon">▤</div><div class="row-main"><strong>${esc(x)}</strong><span>Document client</span></div><button class="icon-btn" data-document="${esc(x)}">◉</button></div>`).join("")}</div></div></article></div></section>`}
function locationScreen(){const l=getLocation(state.location)||locations[0],c=getClient(l.client),ints=interventions.filter(i=>i.location===l.id).slice().reverse(),apps=appointments.filter(a=>a.location===l.id).slice().reverse().slice(0,15);return`<section class="screen"><div class="page-head"><div><h1>Fiches lieux</h1><p>Interventions et rendez-vous restent séparés tout en partageant le même lieu.</p></div></div><div class="master-detail"><aside class="card master"><div class="card-head"><div><h2>Lieux</h2><p>${locations.length} lieux</p></div><span>⌖</span></div><div class="master-list">${locations.map(x=>`<button class="master-item ${x.id===l.id?"active":""}" data-location="${x.id}"><strong>${esc(x.name)}</strong><span>${esc(getClient(x.client).name)}</span><small>${esc(x.address)}</small></button>`).join("")}</div></aside><article class="card"><div class="entity-head"><div class="entity-title"><div class="entity-icon">⌖</div><div><span class="badge primary">Site client</span><h1>${esc(l.name)}</h1><p>${esc(l.address)}</p></div></div><button class="btn btn-primary" data-gps="${esc(l.address)}">⌖ GPS</button></div><div class="tabs"><button class="tab active" data-tab="overview">Informations</button><button class="tab" data-tab="contacts">Contacts</button><button class="tab" data-tab="interventions">Interventions</button><button class="tab" data-tab="appointments">Rendez-vous</button><button class="tab" data-tab="documents">Documents</button></div><div class="tab-panel active" data-panel="overview"><div class="stack-lg"><div class="map" style="border:1px solid var(--border);border-radius:12px"><div class="pin"><span>⌖</span></div></div><div class="grid-2"><div class="info"><span>Client associé</span><strong>${esc(c.name)}</strong><button class="btn btn-ghost btn-sm" data-open-client="${c.id}">Voir le client</button></div><div class="info"><span>Adresse complète</span><strong>${esc(l.address)}</strong></div><div class="info"><span>Interventions</span><strong>${ints.length}</strong></div><div class="info"><span>Rendez-vous</span><strong>${apps.length}</strong></div></div><div class="text-box"><h4>Notes du lieu</h4><p>${esc(l.note)}</p></div></div></div><div class="tab-panel" data-panel="contacts"><div class="stack">${c.contacts.map(x=>`<div class="row">${avatar({...x,color:tone(x.name)})}<div class="row-main"><strong>${esc(x.name)}</strong><span>${esc(x.role)} · ${esc(x.phone)}</span></div><button class="icon-btn" data-phone="${x.phone}">☎</button></div>`).join("")}</div></div><div class="tab-panel" data-panel="interventions"><div class="stack">${ints.map(interventionCard).join("")||empty("⚒","Aucune intervention","Aucune intervention n'est associée à ce lieu.")}</div></div><div class="tab-panel" data-panel="appointments"><div class="stack">${apps.map(a=>apptCard(a,true)).join("")||empty("▣","Aucun rendez-vous","Aucun rendez-vous n'est associé à ce lieu.")}</div></div><div class="tab-panel" data-panel="documents"><div class="stack">${l.docs.map(x=>`<div class="row"><div class="row-icon">▤</div><div class="row-main"><strong>${esc(x)}</strong><span>Document du lieu</span></div><button class="icon-btn" data-document="${esc(x)}">◉</button></div>`).join("")}</div></div></article></div></section>`}
/* =========================================================
ÉCRAN — PARAMÈTRES, THÈME ET SYNCHRONISATION
========================================================= */
const colors=["#4f46e5","#0f7b6c","#1468c4","#b94b65","#c05d12","#72502b"];
function settingsScreen(){return`<section class="screen"><div class="page-head"><div><h1>Paramètres Omika</h1><p>Personnalisez l'apparence et contrôlez la synchronisation manuelle.</p></div></div><div class="detail-layout"><div class="stack-lg"><section class="card"><div class="card-head"><div><h2>Apparence</h2><p>Thème et couleur principale</p></div><span>⚙</span></div><div class="card-body stack-lg"><div class="field"><span class="field-title">Mode d'affichage</span><div class="theme-grid"><button class="theme-choice ${state.theme==="light"?"active":""}" data-theme-choice="light"><div class="theme-thumb light"></div><strong>Thème clair</strong></button><button class="theme-choice ${state.theme==="dark"?"active":""}" data-theme-choice="dark"><div class="theme-thumb dark"></div><strong>Thème sombre</strong></button></div></div><div class="field"><span class="field-title">Couleur principale</span><div class="swatches">${colors.map(c=>`<button class="swatch ${state.primary.toLowerCase()===c?"active":""}" style="--c:${c}" data-color="${c}" aria-label="Couleur ${c}"></button>`).join("")}</div><div class="inline" style="margin-top:9px"><input id="customColor" type="color" value="${state.primary}" style="width:48px;padding:3px"><span class="small muted">Couleur personnalisée</span></div></div><div class="cluster"><span class="small faint">Les préférences sont enregistrées localement.</span><button class="btn btn-primary" data-save-theme>✓ Enregistrer</button></div></div></section><section class="card"><div class="card-body"><div class="cluster"><div><h2 style="margin:0;font-size:17px">Synchronisation manuelle</h2><p class="small muted" style="margin:5px 0 0">Interventions, rendez-vous, temps, rapports, notes et médias.</p></div><span style="font-size:22px">↻</span></div><div class="row" style="margin:14px 0"><div class="row-icon">${state.syncing?'<span class="spin"></span>':"✓"}</div><div class="row-main"><strong>${state.syncing?"Synchronisation en cours":"Toutes les données sont à jour"}</strong><span>${state.syncing?"Envoi des données terrain et récupération des mises à jour…":`Dernière synchronisation : ${fmt(state.lastSync,{day:"numeric",month:"short",hour:"2-digit",minute:"2-digit"})}`}</span></div></div><button class="btn btn-primary btn-block" data-sync ${state.syncing?"disabled":""}>${state.syncing?'<span class="spin"></span> Synchronisation…':"↻ Synchroniser maintenant"}</button></div></section></div><aside class="card preview"><div class="card-head"><div><h2>Aperçu</h2><p>Application terrain</p></div><span class="badge primary">En direct</span></div><div class="preview-top"><strong>Omika</strong><span>↻</span></div><div class="preview-body"><div class="preview-card"><div class="line short"></div><div class="line"></div><div class="line"></div><div class="preview-button"></div></div><div class="preview-card" style="margin-top:10px"><div class="line short"></div><div class="line"></div></div></div></aside></div></section>`}
/* =========================================================
RENDU DES ÉCRANS
========================================================= */
function render(){const r=parseRoute(),valid=["home","appointment","intervention","resource","compte","reports","reportform","email","team","search","folders","clients","locations","settings"];state.screen=valid.includes(r.screen)?r.screen:"home";if(["home","team","search","folders","clients","locations","settings"].includes(state.screen))state.origin=state.screen;let html="";
if(state.screen==="appointment"){if(r.param&&getAppt(r.param))state.appointment=r.param;html=appointmentScreen(getAppt(state.appointment)||appointments[0])}
else if(state.screen==="intervention"){if(r.param&&getIntervention(r.param))state.intervention=r.param;html=interventionScreen(getIntervention(state.intervention)||interventions[0])}
else if(state.screen==="resource"){const [iid,kind]=compound(r.param);if(getIntervention(iid))state.intervention=iid;state.resource=["notes","images","documents"].includes(kind)?kind:"notes";html=resourceScreen(getIntervention(state.intervention)||interventions[0],state.resource)}
else if(state.screen==="compte"){if(r.param&&getIntervention(r.param))state.intervention=r.param;html=compteRenduScreen(getIntervention(state.intervention)||interventions[0])}
else if(state.screen==="reports"){if(r.param&&getIntervention(r.param))state.intervention=r.param;html=reportsScreen(getIntervention(state.intervention)||interventions[0])}
else if(state.screen==="reportform"){const [iid,tid]=compound(r.param);if(getIntervention(iid))state.intervention=iid;if(reportTemplates.some(t=>t.id===tid))state.reportTemplate=tid;html=reportFormScreen(getIntervention(state.intervention)||interventions[0],reportTemplates.find(t=>t.id===state.reportTemplate)||reportTemplates[0])}
else if(state.screen==="email"){const [iid,mode,reportId]=compound(r.param);if(getIntervention(iid))state.intervention=iid;state.emailMode=mode==="direct"||emailTemplates.some(t=>t.id===mode)?mode:"direct";state.emailReport=reportId||null;html=emailScreen(getIntervention(state.intervention)||interventions[0],state.emailMode,state.emailReport)}
else html={home:homeScreen,team:teamScreen,search:searchScreen,folders:folderScreen,clients:clientScreen,locations:locationScreen,settings:settingsScreen}[state.screen]();
id("view").innerHTML=html;id("screenTitle").textContent=titles(state.screen);activeNav();showSync();afterRender();window.scrollTo({top:0,behavior:"instant"})}
function afterRender(){if(state.screen==="resource"){const imageInput=id("resourceImageInput"),docInput=id("resourceDocumentInput"),i=getIntervention(state.intervention);if(imageInput)imageInput.onchange=e=>addImages(i,[...e.target.files]);if(docInput)docInput.onchange=e=>addDocuments(i,[...e.target.files])}if(state.screen==="compte"){state.signatures={tech:false,client:false};setupCanvas(id("crSignTech"),"tech");setupCanvas(id("crSignClient"),"client");restoreForm(`omika-compte-${state.intervention}`,"compteRenduForm")}if(state.screen==="reportform")restoreForm(`omika-report-${state.intervention}-${state.reportTemplate}`,"dynamicReportForm");if(state.screen==="email"&&state.emailMode!=="direct"){const form=id("emailForm");form?.addEventListener("input",updateEmailPreview)}}
/* =========================================================
SIGNATURES, BROUILLONS ET FICHIERS
========================================================= */
function setupCanvas(canvas,key){if(!canvas)return;const ctx=canvas.getContext("2d");ctx.lineWidth=4;ctx.lineCap="round";ctx.strokeStyle=getComputedStyle(document.body).getPropertyValue("--text").trim();let draw=false;const p=e=>{const r=canvas.getBoundingClientRect();return{x:(e.clientX-r.left)*canvas.width/r.width,y:(e.clientY-r.top)*canvas.height/r.height}};canvas.onpointerdown=e=>{draw=true;state.signatures[key]=true;canvas.setPointerCapture(e.pointerId);const q=p(e);ctx.beginPath();ctx.moveTo(q.x,q.y)};canvas.onpointermove=e=>{if(!draw)return;const q=p(e);ctx.lineTo(q.x,q.y);ctx.stroke()};canvas.onpointerup=canvas.onpointercancel=()=>{draw=false;ctx.beginPath()}}
function clearSign(key){const c=id(key==="tech"?"crSignTech":"crSignClient");if(c)c.getContext("2d").clearRect(0,0,c.width,c.height);state.signatures[key]=false}
function formObject(form){const data={};new FormData(form).forEach((v,k)=>{data[k]=data[k]?[].concat(data[k],v):v});return data}
function restoreForm(key,formId){const raw=storage.getItem(key),form=id(formId);if(!raw||!form)return;try{const data=JSON.parse(raw);Object.entries(data).forEach(([k,v])=>{form.querySelectorAll(`[name="${CSS.escape(k)}"]`).forEach(n=>{if(n.type==="checkbox")n.checked=Array.isArray(v)?v.includes(n.value):(v===n.value||v==="on");else n.value=Array.isArray(v)?v[0]:v})})}catch(_){storage.removeItem(key)}}
function addImages(i,files){if(!files.length)return;let left=files.length;files.forEach((f,index)=>{const r=new FileReader();r.onload=e=>{i.images.push({id:`user-${Date.now()}-${index}`,name:f.name,url:e.target.result,note:""});if(--left===0){render();toast("Images ajoutées",`${files.length} image(s) ajoutée(s) à l'intervention.`,"ok")}};r.readAsDataURL(f)})}
function addDocuments(i,files){if(!files.length)return;files.forEach(f=>i.documents.push([f.name,`${f.type||"Fichier"} · ${Math.max(1,Math.round(f.size/1024))} Ko`]));render();toast("Documents ajoutés",`${files.length} document(s) ajouté(s) à l'intervention.`,"ok")}
function updateEmailPreview(){const form=id("emailForm"),i=getIntervention(state.intervention),template=emailTemplates.find(t=>t.id===state.emailMode),report=i?.reports.find(r=>r.id===state.emailReport);if(!form||!i||!template)return;const draft=template.build(i,formObject(form),report),subject=id("emailSubjectPreview"),body=id("emailBodyPreview");if(subject)subject.value=draft.subject;if(body)body.value=draft.body}
/* =========================================================
MODALES : CRÉATION D'INTERVENTION ET PLANIFICATION D'UN RDV
========================================================= */
function openInterventionModal(folderId=state.folder){const f=folders.find(x=>x.id===folderId)||folders[0];id("modalFolder").innerHTML=folders.map(x=>`<option value="${x.id}" ${x.id===f.id?"selected":""}>${x.id} · ${esc(x.title)}</option>`).join("");id("modalClient").innerHTML=clients.map(x=>`<option value="${x.id}" ${x.id===f.client?"selected":""}>${esc(x.name)}</option>`).join("");id("modalLocation").innerHTML=locations.map(x=>`<option value="${x.id}" ${f.locations.includes(x.id)?"selected":""}>${esc(x.name)}</option>`).join("");id("modalOwner").innerHTML=collaborators.map(x=>`<option value="${x.id}">${esc(x.name)}</option>`).join("");id("modalWrap").classList.add("open");id("modalWrap").setAttribute("aria-hidden","false")}
function closeInterventionModal(){id("modalWrap").classList.remove("open");id("modalWrap").setAttribute("aria-hidden","true")}
function openAppointmentModal(interventionId){const i=getIntervention(interventionId),c=getClient(i.client),l=getLocation(i.location);id("appointmentIntervention").value=i.id;id("appointmentContext").innerHTML=`<strong>${esc(i.id)} · ${esc(i.title)}</strong><span>${esc(c.name)} · ${esc(l.name)}</span>`;id("appointmentDate").value=iso(add(today,3));id("appointmentFrom").value="09:00";id("appointmentTo").value="10:30";id("appointmentCollaborator").innerHTML=collaborators.map(x=>`<option value="${x.id}" ${x.id===i.owner?"selected":""}>${esc(x.name)}</option>`).join("");id("appointmentModalWrap").classList.add("open");id("appointmentModalWrap").setAttribute("aria-hidden","false")}
function closeAppointmentModal(){id("appointmentModalWrap").classList.remove("open");id("appointmentModalWrap").setAttribute("aria-hidden","true")}
function openSheet(){id("moreSheet").classList.add("open");id("moreSheet").setAttribute("aria-hidden","false")}
function closeSheet(){id("moreSheet").classList.remove("open");id("moreSheet").setAttribute("aria-hidden","true")}
/* =========================================================
INTERACTIONS GLOBALES
========================================================= */
document.addEventListener("click",e=>{const b=e.target.closest("button,[data-open-appointment],[data-open-intervention]");if(!b)return;
if(b.dataset.route){navigate(b.dataset.route,null,{push:false,clear:true});return}
if(b.dataset.openAppointment){navigate("appointment",b.dataset.openAppointment);return}
if(b.dataset.openIntervention){navigate("intervention",b.dataset.openIntervention);return}
if(b.hasAttribute("data-back")){goBack();return}
if(b.dataset.gps!==undefined){openAction("gps",b.dataset.gps);return}
if(b.dataset.phone!==undefined){openAction("phone",b.dataset.phone);return}
if(b.dataset.emailAddress!==undefined){openAction("email",b.dataset.emailAddress);return}
if(b.dataset.document){toast("Aperçu du document",`${b.dataset.document} serait ouvert dans le lecteur intégré.`);return}
if(b.dataset.tab){const card=b.closest(".card");if(card){card.querySelectorAll(".tab").forEach(tab=>tab.classList.toggle("active",tab===b));card.querySelectorAll(".tab-panel").forEach(panel=>panel.classList.toggle("active",panel.dataset.panel===b.dataset.tab))}return}
if(b.dataset.agenda){state.agendaView=b.dataset.agenda;render();return}
if(b.hasAttribute("data-today")){state.date=today;render();return}
if(b.dataset.period){state.date=add(state.date,Number(b.dataset.period)*(state.agendaView==="day"?1:7));if(state.date<rangeStart)state.date=rangeStart;if(state.date>rangeEnd)state.date=rangeEnd;render();return}
if(b.dataset.date){state.date=parse(b.dataset.date);render();return}
if(b.hasAttribute("data-next-appointment")){const a=appointments.find(x=>x.collaborator==="c1"&&x.date>=iso(today)&&x.status!=="completed");a?navigate("appointment",a.id):toast("Agenda terminé","Aucun rendez-vous à venir.");return}
if(b.dataset.deleteTime){const a=getAppt(state.appointment);a.timeEntries=a.timeEntries.filter(x=>x.id!==b.dataset.deleteTime);render();toast("Temps supprimé","La plage de temps a été retirée.");return}
if(b.dataset.resourceKind){const [iid,kind]=compound(b.dataset.resourceKind);navigate("resource",`${iid}|${kind}`);return}
if(b.dataset.savePhoto){const i=getIntervention(state.intervention),img=i.images.find(x=>x.id===b.dataset.savePhoto),input=$(`[data-photo-note="${CSS.escape(b.dataset.savePhoto)}"]`);if(img&&input){img.note=input.value;toast("Note d'image enregistrée","La légende a été mise à jour.","ok")}return}
if(b.dataset.deleteImage){const i=getIntervention(state.intervention);i.images=i.images.filter(x=>x.id!==b.dataset.deleteImage);render();toast("Image supprimée","L'image a été retirée.");return}
if(b.dataset.openCompte){navigate("compte",b.dataset.openCompte);return}
if(b.dataset.openReports){navigate("reports",b.dataset.openReports);return}
if(b.dataset.reportTemplate){navigate("reportform",b.dataset.reportTemplate);return}
if(b.dataset.openEmailContext){navigate("email",b.dataset.openEmailContext);return}
if(b.dataset.emailMode){navigate("email",b.dataset.emailMode,{push:false});return}
if(b.dataset.clearSign){clearSign(b.dataset.clearSign);return}
if(b.hasAttribute("data-save-compte")){const form=id("compteRenduForm");storage.setItem(`omika-compte-${state.intervention}`,JSON.stringify(formObject(form)));toast("Brouillon enregistré","Le compte rendu fixe est sauvegardé localement.","ok");return}
if(b.hasAttribute("data-save-dynamic-report")){const form=id("dynamicReportForm");storage.setItem(`omika-report-${state.intervention}-${state.reportTemplate}`,JSON.stringify(formObject(form)));toast("Brouillon enregistré","Le formulaire du rapport est sauvegardé localement.","ok");return}
if(b.dataset.collab){state.teamCollab=b.dataset.collab;render();return}
if(b.dataset.teamView){state.teamView=b.dataset.teamView;render();return}
if(b.dataset.teamWeek){state.teamWeek=add(state.teamWeek,Number(b.dataset.teamWeek)*7);render();return}
if(b.hasAttribute("data-team-today")){state.teamWeek=weekStart(today);render();return}
if(b.hasAttribute("data-reset-search")){state.search={relation:"all"};render();return}
if(b.dataset.folder){state.folder=b.dataset.folder;render();return}
if(b.dataset.client){state.client=b.dataset.client;render();return}
if(b.dataset.location){state.location=b.dataset.location;render();return}
if(b.dataset.openClient){state.client=b.dataset.openClient;navigate("clients",null,{push:false,clear:true});return}
if(b.dataset.openLocation){state.location=b.dataset.openLocation;navigate("locations",null,{push:false,clear:true});return}
if(b.dataset.addIntervention){openInterventionModal(b.dataset.addIntervention);return}
if(b.dataset.addAppointment){openAppointmentModal(b.dataset.addAppointment);return}
if(b.hasAttribute("data-close-modal")){closeInterventionModal();return}
if(b.hasAttribute("data-close-appointment-modal")){closeAppointmentModal();return}
if(b.dataset.themeChoice){applyTheme(b.dataset.themeChoice,state.primary);render();return}
if(b.dataset.color){applyTheme(state.theme,b.dataset.color);render();return}
if(b.hasAttribute("data-save-theme")){storage.setItem("omika-theme",state.theme);storage.setItem("omika-primary",state.primary);toast("Préférences enregistrées","Le thème Omika sera restauré au prochain démarrage.","ok");return}
if(b.hasAttribute("data-sync")){syncNow();return}
});
document.addEventListener("change",e=>{if(e.target.id==="jumpDate"&&e.target.value){state.date=parse(e.target.value);render()}if(e.target.id==="customColor"){applyTheme(state.theme,e.target.value);render()}});
document.addEventListener("keydown",e=>{if((e.key==="Enter"||e.key===" ")&&e.target.matches(".appt")){e.preventDefault();navigate("appointment",e.target.dataset.openAppointment)}if(e.key==="Escape"){closeSheet();closeInterventionModal();closeAppointmentModal()}});
id("view").addEventListener("submit",e=>{e.preventDefault();
if(e.target.id==="searchForm"){state.search=searchValues();render();return}
if(e.target.id==="timeForm"){if(!e.target.checkValidity())return e.target.reportValidity();const d=Object.fromEntries(new FormData(e.target));if(timeToMinutes(d.end)<=timeToMinutes(d.start))return toast("Horaires invalides","L'heure de fin doit être postérieure à l'heure de début.","error");const a=getAppt(state.appointment);a.timeEntries.push({id:`TEM-${timeEntrySeq++}`,type:d.type,start:d.start,end:d.end,note:d.note||""});render();toast("Temps ajouté",`${timeTypeInfo[d.type][0]} · ${d.start}–${d.end}`,"ok");return}
if(e.target.id==="resourceNoteForm"){if(!e.target.checkValidity())return e.target.reportValidity();const text=new FormData(e.target).get("note").trim();const i=getIntervention(state.intervention);i.notes.unshift({author:"Camille Bernard",date:iso(today),text});render();toast("Note ajoutée","La note interne a été ajoutée à l'intervention.","ok");return}
if(e.target.id==="compteRenduForm"){if(!e.target.checkValidity())return e.target.reportValidity();if(!state.signatures.tech||!state.signatures.client)return toast("Signatures manquantes","Les signatures du technicien et du client sont obligatoires.","error");const i=getIntervention(state.intervention);i.compteRendu={status:"Validé",createdAt:iso(today),reference:`CR-${today.getFullYear()}-${String(100+Number(i.id.slice(-3))).padStart(4,"0")}`};storage.removeItem(`omika-compte-${i.id}`);toast("Compte rendu validé","Le formulaire fixe et les deux signatures sont enregistrés.","ok");state.navStack.pop();navigate("intervention",i.id,{push:false});return}
if(e.target.id==="dynamicReportForm"){if(!e.target.checkValidity())return e.target.reportValidity();const i=getIntervention(state.intervention),t=reportTemplates.find(x=>x.id===e.target.dataset.template),rid=`RAP-${today.getFullYear()}-${String(600+i.reports.length+Number(i.id.slice(-2))).padStart(4,"0")}`;i.reports.unshift({id:rid,templateId:t.id,title:t.name,createdAt:iso(today),status:"Créé",data:formObject(e.target)});storage.removeItem(`omika-report-${i.id}-${t.id}`);toast("Rapport créé",`${rid} est disponible et peut être envoyé par email.`,"ok");state.navStack.pop();navigate("reports",i.id,{push:false});return}
if(e.target.id==="emailForm"){if(!e.target.checkValidity())return e.target.reportValidity();const i=getIntervention(state.intervention),data=formObject(e.target),template=emailTemplates.find(t=>t.id===e.target.dataset.mode),report=i.reports.find(r=>r.id===e.target.dataset.report),draft=template?template.build(i,data,report):{subject:data.subject,body:data.body};const query=new URLSearchParams({subject:draft.subject,body:draft.body});if(data.cc)query.set("cc",data.cc);toast("Email créé","Le client de messagerie va s'ouvrir avec l'objet et le corps préparés.","ok");setTimeout(()=>{location.href=`mailto:${encodeURIComponent(data.to)}?${query.toString()}`},120);return}
});
id("newInterventionForm").addEventListener("submit",e=>{e.preventDefault();if(!e.target.checkValidity())return e.target.reportValidity();const d=Object.fromEntries(new FormData(e.target)),f=folders.find(x=>x.id===d.folder)||folders[0],newId=`INT-${today.getFullYear()}-${String(1101+interventions.length).padStart(4,"0")}`;const item={id:newId,client:d.client,location:d.location,owner:d.owner,status:"planned",priority:d.priority,title:d.title,short:d.description||"Nouvelle intervention créée dans Omika.",detail:d.description||"Description à compléter.",opened:iso(today),due:iso(add(today,30)),internalSummary:"Intervention créée sans rendez-vous. Une planification pourra être ajoutée ultérieurement.",notes:[{author:"Camille Bernard",date:iso(today),text:"Intervention créée depuis le dossier, sans rendez-vous associé."}],documents:[],images:[],reports:[],compteRendu:null};interventions.push(item);f.interventions.push(newId);state.intervention=newId;closeInterventionModal();e.target.reset();toast("Intervention créée",`${newId} a été créé sans rendez-vous.`,"ok");navigate("intervention",newId)});
id("newAppointmentForm").addEventListener("submit",e=>{e.preventDefault();if(!e.target.checkValidity())return e.target.reportValidity();const d=Object.fromEntries(new FormData(e.target)),i=getIntervention(d.intervention);if(timeToMinutes(d.to)<=timeToMinutes(d.from))return toast("Horaires invalides","L'heure de fin doit être postérieure à l'heure de début.","error");const newId=`RDV-${String(1801+appointments.length).padStart(5,"0")}`,a={id:newId,interventionId:i.id,date:d.date,from:d.from,to:d.to,client:i.client,location:i.location,collaborator:d.collaborator,status:"planned",title:i.title,description:d.description||"Rendez-vous planifié depuis la fiche intervention.",timeEntries:[]};appointments.push(a);appointments.sort((x,y)=>(x.date+x.from).localeCompare(y.date+y.from));state.appointment=newId;closeAppointmentModal();e.target.reset();toast("Rendez-vous planifié",`${newId} est lié à ${i.id}.`,"ok");navigate("appointment",newId)});
id("moreButton").onclick=openSheet;id("menuButton").onclick=openSheet;id("closeSheet").onclick=closeSheet;id("moreSheet").onclick=e=>{if(e.target===id("moreSheet"))closeSheet()};id("modalWrap").onclick=e=>{if(e.target===id("modalWrap"))closeInterventionModal()};id("appointmentModalWrap").onclick=e=>{if(e.target===id("appointmentModalWrap"))closeAppointmentModal()};id("syncTop").onclick=syncNow;id("syncMobile").onclick=syncNow;id("themeTop").onclick=()=>{applyTheme(state.theme==="dark"?"light":"dark",state.primary);render()};window.addEventListener("hashchange",render);applyTheme();showSync();if(!location.hash)location.hash="#home";else render();
</script>
</body>
</html>