Files
Omika/docs/maquetteOmika.html
T
2026-10-23 18:46:50 +02:00

623 lines
144 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>