Refonte design V1
This commit is contained in:
@@ -83,6 +83,16 @@ obligatoire, lien facultatif, période en heure de Paris, priorité, groupes AD
|
|||||||
- Aucun cache à purger : l'add-in interroge l'API à chaque nouveau message, la bannière apparaît et disparaît
|
- Aucun cache à purger : l'add-in interroge l'API à chaque nouveau message, la bannière apparaît et disparaît
|
||||||
donc à l'heure. Les messages déjà envoyés la conservent tant que l'image reste dans la bibliothèque.
|
donc à l'heure. Les messages déjà envoyés la conservent tant que l'image reste dans la bibliothèque.
|
||||||
|
|
||||||
|
## Interface du back-office
|
||||||
|
|
||||||
|
Aucune dépendance externe ni étape de compilation : tout est servi depuis `backend/public`.
|
||||||
|
|
||||||
|
- `css/app.css` : feuille unique. Les couleurs, rayons et polices sont des variables en tête de fichier (`:root`).
|
||||||
|
- `js/app.js` : notifications, boîte de confirmation (`data-confirm` sur un formulaire), zone de dépôt (`data-dropzone`),
|
||||||
|
copie (`data-copy`), bouton occupé (`data-busy`). Sans JavaScript, les formulaires restent utilisables.
|
||||||
|
- `fonts/` : Atkinson Hyperlegible Next et Mono (licence OFL, fichier joint), hébergées localement.
|
||||||
|
- `resources/views/components/icon.blade.php` : icônes Lucide (licence ISC) intégrées, `<x-icon name="plus" />`.
|
||||||
|
|
||||||
## Syntaxe des templates
|
## Syntaxe des templates
|
||||||
|
|
||||||
| Syntaxe | Effet |
|
| Syntaxe | Effet |
|
||||||
|
|||||||
@@ -0,0 +1,416 @@
|
|||||||
|
/*
|
||||||
|
* Back-office et portail « Signatures ».
|
||||||
|
* Feuille unique, sans préprocesseur ni CDN : modifiez les variables ci-dessous pour adapter les couleurs.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Atkinson Hyperlegible Next";
|
||||||
|
src: url("../fonts/atkinson-next.woff2") format("woff2");
|
||||||
|
font-weight: 200 800;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "Atkinson Hyperlegible Mono";
|
||||||
|
src: url("../fonts/atkinson-mono.woff2") format("woff2");
|
||||||
|
font-weight: 200 800;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* Texte et surfaces */
|
||||||
|
--ink: #17242e;
|
||||||
|
--muted: #586a77;
|
||||||
|
--line: #dce3e8;
|
||||||
|
--line-strong: #bfcbd4;
|
||||||
|
--bg: #f2f5f7;
|
||||||
|
--surface: #ffffff;
|
||||||
|
--surface-2: #f7f9fa;
|
||||||
|
|
||||||
|
/* Bleu nuit de la barre latérale, bleu canal des actions, jaune soleil en repère */
|
||||||
|
--night: #0f3554;
|
||||||
|
--night-2: #1a4871;
|
||||||
|
--night-ink: #c9dbe9;
|
||||||
|
--accent: #16649a;
|
||||||
|
--accent-strong: #0f4f7d;
|
||||||
|
--accent-soft: #e4f0f8;
|
||||||
|
--sun: #f2b544;
|
||||||
|
|
||||||
|
/* États */
|
||||||
|
--ok: #1c7440;
|
||||||
|
--ok-soft: #e2f3e8;
|
||||||
|
--warn: #8a5300;
|
||||||
|
--warn-soft: #fff1d2;
|
||||||
|
--danger: #b3261e;
|
||||||
|
--danger-soft: #fdeceb;
|
||||||
|
|
||||||
|
--r-s: 6px;
|
||||||
|
--r-m: 10px;
|
||||||
|
--r-l: 14px;
|
||||||
|
--sidebar: 248px;
|
||||||
|
--focus: 0 0 0 3px rgba(22, 100, 154, .28);
|
||||||
|
|
||||||
|
--font: "Atkinson Hyperlegible Next", "Segoe UI", system-ui, sans-serif;
|
||||||
|
--mono: "Atkinson Hyperlegible Mono", Consolas, "Cascadia Mono", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
*, *::before, *::after { box-sizing: border-box; }
|
||||||
|
|
||||||
|
html { -webkit-text-size-adjust: 100%; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: var(--font);
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.45;
|
||||||
|
color: var(--ink);
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 { font-size: 26px; line-height: 1.15; font-weight: 700; letter-spacing: -.01em; margin: 0; }
|
||||||
|
h2 { font-size: 16px; line-height: 1.3; font-weight: 700; margin: 0 0 12px; }
|
||||||
|
p { margin: 0 0 12px; }
|
||||||
|
a { color: var(--accent); text-underline-offset: 2px; }
|
||||||
|
a:hover { color: var(--accent-strong); }
|
||||||
|
code { font-family: var(--mono); font-size: .9em; background: var(--surface-2); border: 1px solid var(--line); border-radius: 4px; padding: 0 4px; }
|
||||||
|
strong { font-weight: 700; }
|
||||||
|
|
||||||
|
:focus-visible { outline: none; box-shadow: var(--focus); border-radius: var(--r-s); }
|
||||||
|
|
||||||
|
.icon { flex: none; vertical-align: -3px; }
|
||||||
|
.muted { color: var(--muted); font-size: 13px; }
|
||||||
|
.spacer { flex: 1; }
|
||||||
|
.row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
||||||
|
.nowrap { white-space: nowrap; }
|
||||||
|
.right { text-align: right; }
|
||||||
|
[hidden] { display: none !important; }
|
||||||
|
|
||||||
|
.skip { position: absolute; left: -9999px; }
|
||||||
|
.skip:focus { left: 12px; top: 12px; z-index: 50; background: var(--surface); padding: 8px 12px; border-radius: var(--r-s); }
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------- Structure */
|
||||||
|
|
||||||
|
.app { min-height: 100vh; }
|
||||||
|
.app--nav { display: grid; grid-template-columns: var(--sidebar) minmax(0, 1fr); }
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
position: sticky; top: 0; height: 100vh; overflow-y: auto;
|
||||||
|
display: flex; flex-direction: column;
|
||||||
|
background: var(--night); color: var(--night-ink);
|
||||||
|
padding: 20px 14px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand { display: block; color: #fff; text-decoration: none; padding: 0 8px 18px; }
|
||||||
|
.brand:hover { color: #fff; }
|
||||||
|
.brand-name { display: block; font-size: 21px; font-weight: 700; letter-spacing: -.01em; line-height: 1.1; }
|
||||||
|
.brand-stroke { display: block; width: 104px; height: 12px; margin: 3px 0 6px -2px; color: var(--sun); }
|
||||||
|
.brand-org { display: block; font-size: 12.5px; color: var(--night-ink); }
|
||||||
|
|
||||||
|
.nav { display: flex; flex-direction: column; gap: 2px; }
|
||||||
|
.nav-group { font-size: 12px; color: #9db6ca; margin: 16px 0 4px; padding: 0 10px; }
|
||||||
|
.nav-link {
|
||||||
|
position: relative;
|
||||||
|
display: flex; align-items: center; gap: 10px;
|
||||||
|
padding: 8px 10px; border-radius: var(--r-s);
|
||||||
|
color: var(--night-ink); text-decoration: none; font-weight: 500;
|
||||||
|
}
|
||||||
|
.nav-link:hover { background: var(--night-2); color: #fff; }
|
||||||
|
.nav-link.active { background: var(--night-2); color: #fff; font-weight: 700; }
|
||||||
|
.nav-link.active::before { content: ""; position: absolute; left: -14px; top: 7px; bottom: 7px; width: 4px; border-radius: 0 4px 4px 0; background: var(--sun); }
|
||||||
|
.nav-link:focus-visible { box-shadow: 0 0 0 3px rgba(242, 181, 68, .55); }
|
||||||
|
|
||||||
|
.sidebar-foot { margin-top: auto; padding-top: 16px; display: flex; flex-direction: column; gap: 10px; }
|
||||||
|
.user { display: flex; align-items: center; gap: 10px; padding: 10px 8px 2px; border-top: 1px solid rgba(255, 255, 255, .14); }
|
||||||
|
.user-name { display: block; color: #fff; font-weight: 600; font-size: 14px; line-height: 1.2; overflow-wrap: anywhere; }
|
||||||
|
.user-role { display: block; font-size: 12.5px; color: #9db6ca; }
|
||||||
|
.user form { margin-left: auto; }
|
||||||
|
.icon-btn {
|
||||||
|
display: inline-flex; align-items: center; justify-content: center;
|
||||||
|
width: 34px; height: 34px; border-radius: var(--r-s);
|
||||||
|
background: transparent; border: 0; color: inherit; cursor: pointer;
|
||||||
|
}
|
||||||
|
.icon-btn:hover { background: rgba(127, 148, 165, .22); }
|
||||||
|
.sidebar .icon-btn { color: var(--night-ink); }
|
||||||
|
.sidebar .icon-btn:hover { background: var(--night-2); color: #fff; }
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
flex: none; display: inline-flex; align-items: center; justify-content: center;
|
||||||
|
width: 34px; height: 34px; border-radius: 50%;
|
||||||
|
background: var(--accent-soft); color: var(--accent-strong);
|
||||||
|
font-size: 12.5px; font-weight: 700;
|
||||||
|
}
|
||||||
|
.sidebar .avatar { background: var(--night-2); color: #fff; }
|
||||||
|
|
||||||
|
.content { min-width: 0; }
|
||||||
|
main { max-width: 1180px; margin: 0 auto; padding: 28px 28px 56px; }
|
||||||
|
|
||||||
|
/* Portail agent : pas de barre latérale, simple bandeau */
|
||||||
|
.topbar {
|
||||||
|
display: flex; align-items: center; gap: 12px;
|
||||||
|
background: var(--night); color: var(--night-ink); padding: 10px 20px;
|
||||||
|
}
|
||||||
|
.topbar .brand { padding: 0; }
|
||||||
|
.topbar .brand-stroke { display: none; }
|
||||||
|
.topbar .brand-name { font-size: 18px; }
|
||||||
|
.topbar .user { border: 0; padding: 0; margin-left: auto; }
|
||||||
|
.topbar .icon-btn { color: var(--night-ink); }
|
||||||
|
.topbar .icon-btn:hover { background: var(--night-2); color: #fff; }
|
||||||
|
.topbar .avatar { background: var(--night-2); color: #fff; }
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.app--nav { display: block; }
|
||||||
|
.sidebar { position: static; height: auto; padding: 12px 12px 8px; }
|
||||||
|
.brand { padding: 0 6px 8px; }
|
||||||
|
.brand-stroke, .brand-org, .nav-group { display: none; }
|
||||||
|
.nav { flex-direction: row; overflow-x: auto; gap: 4px; padding-bottom: 6px; scrollbar-width: thin; }
|
||||||
|
.nav-link { white-space: nowrap; }
|
||||||
|
.nav-link.active::before { left: 10px; right: 10px; top: auto; bottom: 2px; width: auto; height: 3px; border-radius: 3px; }
|
||||||
|
.sidebar-foot { flex-direction: row; align-items: center; padding-top: 6px; }
|
||||||
|
.sidebar-foot .user { border: 0; padding: 0; margin-left: auto; }
|
||||||
|
.user-role { display: none; }
|
||||||
|
main { padding: 18px 14px 40px; }
|
||||||
|
h1 { font-size: 22px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------- En-tête de page */
|
||||||
|
|
||||||
|
.page-head { display: flex; align-items: flex-start; gap: 16px; flex-wrap: wrap; margin-bottom: 20px; }
|
||||||
|
.page-head > div:first-child { flex: 1; min-width: 240px; }
|
||||||
|
.page-head .lead { color: var(--muted); max-width: 78ch; margin: 8px 0 0; }
|
||||||
|
.page-title { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
||||||
|
.page-actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
||||||
|
.back { display: inline-flex; align-items: center; gap: 4px; font-size: 13.5px; text-decoration: none; margin-bottom: 6px; color: var(--muted); }
|
||||||
|
.back:hover { color: var(--accent-strong); }
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------- Cartes */
|
||||||
|
|
||||||
|
.card { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-m); padding: 20px; margin-bottom: 16px; }
|
||||||
|
.card-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }
|
||||||
|
.card-head h2 { margin: 0; }
|
||||||
|
.card--flush { padding: 0; overflow: hidden; }
|
||||||
|
.card--flush > .card-head { padding: 16px 20px 0; }
|
||||||
|
.card-foot { padding: 12px 20px; border-top: 1px solid var(--line); background: var(--surface-2); }
|
||||||
|
.card--flush > .card-foot:last-child { border-radius: 0 0 var(--r-m) var(--r-m); }
|
||||||
|
|
||||||
|
.split { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 16px; align-items: start; }
|
||||||
|
@media (max-width: 1000px) { .split { grid-template-columns: 1fr; } }
|
||||||
|
|
||||||
|
/* Indicateurs du tableau de bord */
|
||||||
|
.tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 12px; margin-bottom: 16px; }
|
||||||
|
.tile {
|
||||||
|
display: flex; align-items: center; gap: 12px;
|
||||||
|
background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-m);
|
||||||
|
padding: 14px 16px; color: inherit; text-decoration: none;
|
||||||
|
}
|
||||||
|
a.tile:hover { border-color: var(--accent); color: inherit; }
|
||||||
|
.tile-icon { flex: none; display: inline-flex; align-items: center; justify-content: center; width: 40px; height: 40px; border-radius: var(--r-s); background: var(--accent-soft); color: var(--accent-strong); }
|
||||||
|
.tile-value { display: block; font-size: 24px; font-weight: 700; line-height: 1.1; font-variant-numeric: tabular-nums; }
|
||||||
|
.tile-label { display: block; color: var(--muted); font-size: 13px; }
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------- Tableaux */
|
||||||
|
|
||||||
|
.table-wrap { overflow-x: auto; }
|
||||||
|
table.list { width: 100%; border-collapse: collapse; }
|
||||||
|
table.list th, table.list td { text-align: left; padding: 11px 12px; border-bottom: 1px solid var(--line); vertical-align: middle; }
|
||||||
|
table.list th { font-size: 12.5px; font-weight: 600; color: var(--muted); background: var(--surface-2); white-space: nowrap; }
|
||||||
|
table.list th:first-child, table.list td:first-child { padding-left: 20px; }
|
||||||
|
table.list th:last-child, table.list td:last-child { padding-right: 20px; }
|
||||||
|
table.list tbody tr:last-child td { border-bottom: 0; }
|
||||||
|
table.list tbody tr:hover td { background: #fafcfd; }
|
||||||
|
table.list .num { font-variant-numeric: tabular-nums; }
|
||||||
|
.cell-title { font-weight: 600; text-decoration: none; }
|
||||||
|
.cell-title:hover { text-decoration: underline; }
|
||||||
|
.cell-actions { text-align: right; white-space: nowrap; }
|
||||||
|
.cell-actions > * { margin: 2px 0 2px 4px; }
|
||||||
|
.btn.icon-only { padding: 6px 8px; }
|
||||||
|
.person { display: flex; align-items: center; gap: 10px; }
|
||||||
|
|
||||||
|
/* Fiche descriptive (libellé / valeur) */
|
||||||
|
dl.facts { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 0; margin: 0; }
|
||||||
|
dl.facts dt, dl.facts dd { padding: 9px 0; border-bottom: 1px solid var(--line); margin: 0; }
|
||||||
|
dl.facts dt { color: var(--muted); font-size: 13px; padding-right: 20px; }
|
||||||
|
dl.facts dt:last-of-type, dl.facts dd:last-of-type { border-bottom: 0; }
|
||||||
|
|
||||||
|
.empty { text-align: center; padding: 36px 20px; color: var(--muted); }
|
||||||
|
.empty .icon { color: var(--line-strong); margin-bottom: 8px; }
|
||||||
|
.empty p { margin: 0 auto 12px; max-width: 46ch; }
|
||||||
|
.empty p:last-child { margin-bottom: 0; }
|
||||||
|
.empty strong { display: block; color: var(--ink); font-size: 16px; margin-bottom: 4px; }
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------- Formulaires */
|
||||||
|
|
||||||
|
label { display: block; font-weight: 600; font-size: 13.5px; margin: 14px 0 5px; }
|
||||||
|
.card > label:first-child, .card > h2 + label { margin-top: 0; }
|
||||||
|
.hint { color: var(--muted); font-size: 13px; margin: 5px 0 0; }
|
||||||
|
|
||||||
|
input[type=text], input[type=number], input[type=search], input[type=url], input[type=datetime-local], select, textarea {
|
||||||
|
width: 100%; padding: 8px 10px;
|
||||||
|
border: 1px solid var(--line-strong); border-radius: var(--r-s);
|
||||||
|
font: inherit; color: var(--ink); background: var(--surface);
|
||||||
|
}
|
||||||
|
input:hover, select:hover, textarea:hover { border-color: #93a5b2; }
|
||||||
|
input:focus-visible, select:focus-visible, textarea:focus-visible { border-color: var(--accent); box-shadow: var(--focus); }
|
||||||
|
input::placeholder { color: #8797a3; }
|
||||||
|
input.small { width: 96px; }
|
||||||
|
select { padding-right: 28px; }
|
||||||
|
textarea.code { font-family: var(--mono); font-size: 13px; line-height: 1.5; min-height: 280px; resize: vertical; tab-size: 2; background: var(--surface-2); }
|
||||||
|
|
||||||
|
fieldset { border: 1px solid var(--line); border-radius: var(--r-s); margin: 14px 0 0; padding: 8px 14px 12px; min-width: 0; }
|
||||||
|
fieldset.bare { border: 0; margin: 0; padding: 0; }
|
||||||
|
legend { font-weight: 600; font-size: 13.5px; padding: 0 6px; }
|
||||||
|
.scroll { max-height: 230px; overflow: auto; }
|
||||||
|
|
||||||
|
.field-row { display: flex; gap: 12px; flex-wrap: wrap; }
|
||||||
|
.field-row > * { flex: 1; min-width: 190px; }
|
||||||
|
|
||||||
|
/* Champ de recherche avec icône */
|
||||||
|
.search { position: relative; flex: 1; max-width: 380px; }
|
||||||
|
.search .icon { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); color: var(--muted); }
|
||||||
|
.search input { padding-left: 34px; }
|
||||||
|
|
||||||
|
/* Cases et boutons radio */
|
||||||
|
label.check { display: flex; align-items: flex-start; gap: 8px; font-weight: 400; font-size: 14.5px; margin: 6px 0; cursor: pointer; }
|
||||||
|
label.check input { flex: none; width: 17px; height: 17px; margin: 2px 0 0; accent-color: var(--accent); }
|
||||||
|
|
||||||
|
/* Interrupteur : une case à cocher habillée */
|
||||||
|
.switch { display: inline-flex; align-items: center; gap: 10px; font-weight: 400; font-size: 14.5px; margin: 0; cursor: pointer; }
|
||||||
|
.switch input { position: absolute; opacity: 0; width: 1px; height: 1px; }
|
||||||
|
.switch-track { flex: none; position: relative; width: 38px; height: 22px; border-radius: 11px; background: var(--line-strong); transition: background .15s; }
|
||||||
|
.switch-track::after { content: ""; position: absolute; top: 3px; left: 3px; width: 16px; height: 16px; border-radius: 50%; background: #fff; transition: transform .15s; }
|
||||||
|
.switch input:checked + .switch-track { background: var(--accent); }
|
||||||
|
.switch input:checked + .switch-track::after { transform: translateX(16px); }
|
||||||
|
.switch input:focus-visible + .switch-track { box-shadow: var(--focus); }
|
||||||
|
.switch input:disabled + .switch-track { opacity: .5; }
|
||||||
|
|
||||||
|
/* Même interrupteur sous forme de bouton : il envoie son formulaire au clic (activation d'une campagne) */
|
||||||
|
button.switch-btn { position: relative; width: 38px; height: 22px; padding: 0; border: 0; border-radius: 11px; background: var(--line-strong); cursor: pointer; vertical-align: middle; transition: background .15s; }
|
||||||
|
button.switch-btn::after { content: ""; position: absolute; top: 3px; left: 3px; width: 16px; height: 16px; border-radius: 50%; background: #fff; transition: transform .15s; }
|
||||||
|
button.switch-btn[aria-checked=true] { background: var(--accent); }
|
||||||
|
button.switch-btn[aria-checked=true]::after { transform: translateX(16px); }
|
||||||
|
|
||||||
|
/* Choix exclusifs présentés en liste de cartes (portail) */
|
||||||
|
.option { display: flex; align-items: flex-start; gap: 10px; padding: 9px 12px; margin: 0; border: 1px solid var(--line); border-radius: var(--r-s); font-weight: 400; font-size: 14.5px; cursor: pointer; }
|
||||||
|
.option + .option { margin-top: 6px; }
|
||||||
|
fieldset > .option:first-of-type { margin-top: 4px; }
|
||||||
|
.option input[type=radio] { flex: none; width: 17px; height: 17px; margin: 2px 0 0; accent-color: var(--accent); }
|
||||||
|
.option > span { min-width: 0; }
|
||||||
|
.option:has(input:checked) { border-color: var(--accent); background: var(--accent-soft); }
|
||||||
|
|
||||||
|
/* Zone de dépôt de fichier */
|
||||||
|
.dropzone {
|
||||||
|
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px;
|
||||||
|
min-height: 112px; padding: 16px; margin: 0;
|
||||||
|
border: 1.5px dashed var(--line-strong); border-radius: var(--r-m);
|
||||||
|
background: var(--surface-2); color: var(--muted);
|
||||||
|
font-weight: 400; font-size: 14px; text-align: center; cursor: pointer;
|
||||||
|
}
|
||||||
|
.dropzone:hover, .dropzone.is-over { border-color: var(--accent); background: var(--accent-soft); color: var(--accent-strong); }
|
||||||
|
.dropzone:focus-within { border-color: var(--accent); box-shadow: var(--focus); }
|
||||||
|
.dropzone input[type=file] { position: absolute; opacity: 0; width: 1px; height: 1px; }
|
||||||
|
.dropzone-name { color: var(--ink); font-weight: 600; overflow-wrap: anywhere; }
|
||||||
|
.dropzone u { color: var(--accent); }
|
||||||
|
html:not(.js) .dropzone input[type=file] { position: static; opacity: 1; width: auto; height: auto; }
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------- Boutons */
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: inline-flex; align-items: center; justify-content: center; gap: 7px;
|
||||||
|
padding: 8px 14px; border-radius: var(--r-s);
|
||||||
|
border: 1px solid var(--accent); background: var(--accent); color: #fff;
|
||||||
|
font: inherit; font-size: 14.5px; font-weight: 600; line-height: 1.2;
|
||||||
|
text-decoration: none; cursor: pointer; white-space: nowrap;
|
||||||
|
}
|
||||||
|
.btn:hover { background: var(--accent-strong); border-color: var(--accent-strong); color: #fff; }
|
||||||
|
.btn.secondary { background: var(--surface); color: var(--accent-strong); border-color: var(--line-strong); }
|
||||||
|
.btn.secondary:hover { background: var(--accent-soft); border-color: var(--accent); }
|
||||||
|
.btn.danger { background: var(--surface); color: var(--danger); border-color: #e3b5b1; }
|
||||||
|
.btn.danger:hover { background: var(--danger-soft); border-color: var(--danger); }
|
||||||
|
.btn.danger.solid { background: var(--danger); color: #fff; border-color: var(--danger); }
|
||||||
|
.btn.danger.solid:hover { background: #8f1d17; }
|
||||||
|
.btn.small { padding: 5px 10px; font-size: 13.5px; }
|
||||||
|
.btn:disabled, .btn[aria-disabled=true] { opacity: .5; cursor: default; pointer-events: none; }
|
||||||
|
.btn .icon { vertical-align: 0; }
|
||||||
|
.btn.is-busy .icon { animation: spin .8s linear infinite; }
|
||||||
|
@keyframes spin { to { transform: rotate(360deg); } }
|
||||||
|
|
||||||
|
button.link { background: none; border: 0; color: var(--accent); cursor: pointer; font: inherit; padding: 0; }
|
||||||
|
form.inline { display: inline; }
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------- Étiquettes et messages */
|
||||||
|
|
||||||
|
.badge { display: inline-flex; align-items: center; gap: 6px; padding: 2px 9px; border-radius: 999px; font-size: 12.5px; font-weight: 600; background: var(--accent-soft); color: var(--accent-strong); white-space: nowrap; }
|
||||||
|
.badge::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
||||||
|
.badge.ok { background: var(--ok-soft); color: var(--ok); }
|
||||||
|
.badge.warn { background: var(--warn-soft); color: var(--warn); }
|
||||||
|
.badge.off { background: #edf1f4; color: var(--muted); }
|
||||||
|
.badge.plain::before { display: none; }
|
||||||
|
|
||||||
|
.alert { display: flex; gap: 10px; align-items: flex-start; padding: 11px 14px; border-radius: var(--r-s); margin-bottom: 16px; border: 1px solid; }
|
||||||
|
.alert .icon { margin-top: 1px; }
|
||||||
|
.alert > div { min-width: 0; }
|
||||||
|
.notice { background: var(--warn-soft); color: var(--warn); border-color: #ecd28f; }
|
||||||
|
.errors { background: var(--danger-soft); color: var(--danger); border-color: #f0c0bb; }
|
||||||
|
.flash { background: var(--ok-soft); color: var(--ok); border-color: #b7dfc5; }
|
||||||
|
|
||||||
|
/* Notification de confirmation (message de session) */
|
||||||
|
.toasts { position: fixed; bottom: 20px; right: 20px; z-index: 40; display: flex; flex-direction: column; gap: 8px; max-width: min(460px, calc(100vw - 32px)); }
|
||||||
|
.toast {
|
||||||
|
display: flex; gap: 10px; align-items: flex-start;
|
||||||
|
background: var(--ink); color: #fff; padding: 12px 12px 12px 14px;
|
||||||
|
border-radius: var(--r-m); box-shadow: 0 10px 30px rgba(15, 53, 84, .28);
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
.toast > .icon { color: #7fd9a0; margin-top: 1px; }
|
||||||
|
.toast .icon-btn { width: 26px; height: 26px; margin: -3px -3px 0 auto; color: #c9d5de; }
|
||||||
|
.toast.is-leaving { opacity: 0; transform: translateY(6px); transition: opacity .2s, transform .2s; }
|
||||||
|
html:not(.js) .toasts { position: static; max-width: none; margin-bottom: 16px; }
|
||||||
|
html:not(.js) .toast .icon-btn { display: none; }
|
||||||
|
|
||||||
|
/* Boîte de confirmation */
|
||||||
|
dialog.dialog { border: 0; border-radius: var(--r-l); padding: 0; width: min(440px, calc(100vw - 32px)); color: var(--ink); box-shadow: 0 24px 60px rgba(15, 53, 84, .35); }
|
||||||
|
dialog.dialog::backdrop { background: rgba(15, 35, 50, .45); }
|
||||||
|
.dialog-body { padding: 22px 22px 6px; }
|
||||||
|
.dialog-body h2 { font-size: 18px; margin-bottom: 8px; }
|
||||||
|
.dialog-body p { color: var(--muted); margin: 0; }
|
||||||
|
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; padding: 18px 22px 20px; }
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------- Écrans */
|
||||||
|
|
||||||
|
/* Variables cliquables de l'éditeur de template */
|
||||||
|
.chips { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||||
|
.chips button { background: var(--accent-soft); color: var(--accent-strong); border: 1px solid transparent; border-radius: var(--r-s); padding: 3px 9px; font: 13px var(--mono); cursor: pointer; }
|
||||||
|
.chips button:hover { border-color: var(--accent); }
|
||||||
|
|
||||||
|
iframe.sig { display: block; width: 100%; min-height: 270px; border: 1px solid var(--line); border-radius: var(--r-s); background: #fff; resize: vertical; }
|
||||||
|
iframe.sig.short { min-height: 150px; }
|
||||||
|
|
||||||
|
/* Bibliothèque d'images */
|
||||||
|
.gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 12px; }
|
||||||
|
.media { display: flex; flex-direction: column; background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-m); overflow: hidden; }
|
||||||
|
.media-thumb {
|
||||||
|
display: flex; align-items: center; justify-content: center; height: 130px; padding: 12px;
|
||||||
|
/* damier : rend visibles les zones transparentes d'un logo */
|
||||||
|
background: #fff repeating-conic-gradient(#eef2f5 0% 25%, #fff 0% 50%) 0 0 / 16px 16px;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
.media-thumb img { max-width: 100%; max-height: 100%; }
|
||||||
|
.media-body { padding: 12px 14px; flex: 1; }
|
||||||
|
.media-name { font-weight: 600; overflow-wrap: anywhere; }
|
||||||
|
.media-foot { display: flex; gap: 6px; padding: 10px 14px; border-top: 1px solid var(--line); background: var(--surface-2); }
|
||||||
|
|
||||||
|
.thumb { display: block; max-width: 88px; max-height: 56px; border: 1px solid var(--line); border-radius: 4px; background: #fff; }
|
||||||
|
.preview-img { display: block; max-width: 100%; max-height: 160px; margin-top: 10px; border: 1px solid var(--line); border-radius: var(--r-s); background: #fff; }
|
||||||
|
|
||||||
|
/* Pagination */
|
||||||
|
.pager { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
||||||
|
|
||||||
|
/* État de la synchronisation */
|
||||||
|
.sync { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
|
||||||
|
.sync-dot { flex: none; width: 10px; height: 10px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 4px var(--ok-soft); }
|
||||||
|
.sync-dot.warn { background: var(--warn); box-shadow: 0 0 0 4px var(--warn-soft); }
|
||||||
|
.sync p { margin: 0; }
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
*, *::before, *::after { animation: none !important; transition: none !important; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2020-2024 The Atkinson Hyperlegible Next Project Authors (https://github.com/googlefonts/atkinson-hyperlegible-next) AtkinsonHyperlegibleNext-Italic[wght].ttf: Copyright 2020-2024 The Atkinson Hyperlegible Next Project Authors (https://github.com/googlefonts/atkinson-hyperlegible-next)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,111 @@
|
|||||||
|
/*
|
||||||
|
* Comportements communs du back-office (aucune dépendance).
|
||||||
|
* Chaque bloc s'active par un attribut data-* : sans JavaScript, les formulaires fonctionnent quand même.
|
||||||
|
*/
|
||||||
|
(() => {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// --- Navigation en bandeau (petits écrans) : l'entrée active reste visible ---------------------
|
||||||
|
const current = document.querySelector('.nav .nav-link.active');
|
||||||
|
if (current && current.parentElement.scrollWidth > current.parentElement.clientWidth) {
|
||||||
|
current.parentElement.scrollLeft = current.offsetLeft - 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Notifications : fermeture manuelle, et automatique après quelques secondes ----------------
|
||||||
|
const dismiss = (toast) => {
|
||||||
|
toast.classList.add('is-leaving');
|
||||||
|
setTimeout(() => toast.remove(), 220);
|
||||||
|
};
|
||||||
|
document.querySelectorAll('[data-toast]').forEach((toast) => {
|
||||||
|
let timer = setTimeout(() => dismiss(toast), 9000);
|
||||||
|
toast.addEventListener('mouseenter', () => clearTimeout(timer));
|
||||||
|
toast.addEventListener('mouseleave', () => { timer = setTimeout(() => dismiss(toast), 4000); });
|
||||||
|
toast.querySelector('[data-dismiss]')?.addEventListener('click', () => { clearTimeout(timer); dismiss(toast); });
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Confirmation : <form data-confirm="Question ?" data-confirm-label="Supprimer" data-confirm-danger>
|
||||||
|
const dialog = document.getElementById('confirm-dialog');
|
||||||
|
if (dialog && typeof dialog.showModal === 'function') {
|
||||||
|
const message = dialog.querySelector('[data-message]');
|
||||||
|
const ok = dialog.querySelector('[data-ok]');
|
||||||
|
let pending = null;
|
||||||
|
|
||||||
|
document.addEventListener('submit', (e) => {
|
||||||
|
const form = e.target;
|
||||||
|
if (!(form instanceof HTMLFormElement) || !form.dataset.confirm || form.dataset.confirmed) return;
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopImmediatePropagation();
|
||||||
|
pending = form;
|
||||||
|
message.textContent = form.dataset.confirm;
|
||||||
|
ok.textContent = form.dataset.confirmLabel || 'Confirmer';
|
||||||
|
ok.classList.toggle('danger', 'confirmDanger' in form.dataset);
|
||||||
|
ok.classList.toggle('solid', 'confirmDanger' in form.dataset);
|
||||||
|
dialog.returnValue = '';
|
||||||
|
dialog.showModal();
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
dialog.addEventListener('close', () => {
|
||||||
|
const form = pending;
|
||||||
|
pending = null;
|
||||||
|
if (!form || dialog.returnValue !== 'ok') return;
|
||||||
|
form.dataset.confirmed = '1';
|
||||||
|
form.requestSubmit ? form.requestSubmit() : form.submit();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Bouton occupé : <form data-busy="Texte pendant l'envoi…"> ----------------------------------
|
||||||
|
document.addEventListener('submit', (e) => {
|
||||||
|
const form = e.target;
|
||||||
|
if (!(form instanceof HTMLFormElement) || !form.dataset.busy || e.defaultPrevented) return;
|
||||||
|
const button = form.querySelector('button[type=submit], button:not([type])');
|
||||||
|
if (!button) return;
|
||||||
|
button.classList.add('is-busy');
|
||||||
|
button.setAttribute('aria-disabled', 'true');
|
||||||
|
const label = button.querySelector('[data-label]');
|
||||||
|
if (label) label.textContent = form.dataset.busy;
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Copie dans le presse-papiers : <button data-copy="texte"> ---------------------------------
|
||||||
|
document.addEventListener('click', async (e) => {
|
||||||
|
const button = e.target.closest('[data-copy]');
|
||||||
|
if (!button) return;
|
||||||
|
const label = button.querySelector('[data-label]') || button;
|
||||||
|
const initial = label.textContent;
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(button.dataset.copy);
|
||||||
|
label.textContent = 'Copié';
|
||||||
|
} catch {
|
||||||
|
label.textContent = 'Copie impossible';
|
||||||
|
}
|
||||||
|
setTimeout(() => { label.textContent = initial; }, 1600);
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Zone de dépôt : <label class="dropzone" data-dropzone><input type="file"> … ---------------
|
||||||
|
document.querySelectorAll('[data-dropzone]').forEach((zone) => {
|
||||||
|
const input = zone.querySelector('input[type=file]');
|
||||||
|
const name = zone.querySelector('[data-name]');
|
||||||
|
const prompt = zone.querySelector('[data-prompt]');
|
||||||
|
if (!input) return;
|
||||||
|
|
||||||
|
const show = () => {
|
||||||
|
const file = input.files[0];
|
||||||
|
if (name) { name.textContent = file ? file.name : ''; name.hidden = !file; }
|
||||||
|
if (prompt) prompt.hidden = Boolean(file);
|
||||||
|
};
|
||||||
|
input.addEventListener('change', show);
|
||||||
|
|
||||||
|
['dragenter', 'dragover'].forEach((type) => zone.addEventListener(type, (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
zone.classList.add('is-over');
|
||||||
|
}));
|
||||||
|
['dragleave', 'drop'].forEach((type) => zone.addEventListener(type, () => zone.classList.remove('is-over')));
|
||||||
|
zone.addEventListener('drop', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (input.disabled || !e.dataTransfer.files.length) return;
|
||||||
|
const transfer = new DataTransfer();
|
||||||
|
transfer.items.add(e.dataTransfer.files[0]);
|
||||||
|
input.files = transfer.files;
|
||||||
|
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -1,40 +1,74 @@
|
|||||||
@extends('layouts.app')
|
@extends('layouts.app')
|
||||||
@section('title', 'Agents — Signatures CABM')
|
@section('title', 'Agents — Signatures')
|
||||||
@section('content')
|
@section('content')
|
||||||
|
@php
|
||||||
|
$initials = fn (?string $name) => mb_strtoupper(collect(preg_split('/[\s\-]+/u', trim((string) $name)))->filter()->take(2)->map(fn ($w) => mb_substr($w, 0, 1))->join(''));
|
||||||
|
@endphp
|
||||||
|
<header class="page-head">
|
||||||
|
<div>
|
||||||
<h1>Agents</h1>
|
<h1>Agents</h1>
|
||||||
|
<p class="lead">Qui reçoit quoi : le template appliqué à chaque agent selon ses groupes. Ouvrez un agent pour voir sa signature.</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
<form method="get" class="row" style="margin-bottom:16px">
|
<form method="get" class="row" style="margin-bottom:16px">
|
||||||
<input type="search" name="q" value="{{ $q }}" placeholder="Nom, adresse ou service" style="max-width:360px" aria-label="Rechercher un agent">
|
<div class="search">
|
||||||
<button class="btn" type="submit">Rechercher</button>
|
<x-icon name="search" :size="16" />
|
||||||
<span class="muted">{{ $agents->total() }} agent(s)</span>
|
<input type="search" name="q" value="{{ $q }}" placeholder="Nom, adresse ou service" aria-label="Rechercher un agent">
|
||||||
|
</div>
|
||||||
|
<button class="btn secondary" type="submit">Rechercher</button>
|
||||||
|
<span class="muted">{{ $agents->total() }} agent{{ $agents->total() > 1 ? 's' : '' }}</span>
|
||||||
</form>
|
</form>
|
||||||
<div class="card">
|
|
||||||
|
<div class="card card--flush">
|
||||||
|
<div class="table-wrap">
|
||||||
<table class="list">
|
<table class="list">
|
||||||
<thead><tr><th>Agent</th><th>Service</th><th>Groupes signature_*</th><th>Template appliqué</th></tr></thead>
|
<thead><tr><th>Agent</th><th>Service</th><th>Groupes signature_*</th><th>Template appliqué</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@forelse ($agents as $a)
|
@forelse ($agents as $a)
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="{{ route('admin.agents.show', $a) }}">{{ $a->display_name }}</a><div class="muted">{{ $a->mail }}</div></td>
|
<td>
|
||||||
|
<div class="person">
|
||||||
|
<span class="avatar" aria-hidden="true">{{ $initials($a->display_name) }}</span>
|
||||||
|
<div>
|
||||||
|
<a class="cell-title" href="{{ route('admin.agents.show', $a) }}">{{ $a->display_name }}</a>
|
||||||
|
<div class="muted">{{ $a->mail }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<td>{{ $a->department }}</td>
|
<td>{{ $a->department }}</td>
|
||||||
<td class="muted">{{ $a->groups->pluck('display_name')->join(', ') ?: '—' }}</td>
|
<td class="muted">{{ $a->groups->pluck('display_name')->join(', ') ?: '—' }}</td>
|
||||||
<td>{{ $resolved[$a->id] ?? '— aucun —' }}</td>
|
<td>
|
||||||
|
@if ($resolved[$a->id] ?? null)
|
||||||
|
{{ $resolved[$a->id] }}
|
||||||
|
@else
|
||||||
|
<span class="badge warn">aucun</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@empty
|
@empty
|
||||||
<tr><td colspan="4" class="muted">Aucun agent trouvé.</td></tr>
|
<tr><td colspan="4">
|
||||||
|
<div class="empty">
|
||||||
|
<x-icon name="search" :size="32" />
|
||||||
|
<p><strong>Aucun agent trouvé</strong>{{ $q !== '' ? 'Essayez un autre nom, une adresse ou un service.' : 'Lancez une synchronisation de l\'annuaire depuis le tableau de bord.' }}</p>
|
||||||
|
</div>
|
||||||
|
</td></tr>
|
||||||
@endforelse
|
@endforelse
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
@if ($agents->hasPages())
|
@if ($agents->hasPages())
|
||||||
<nav class="row" style="margin-top:12px" aria-label="Pagination">
|
<nav class="card-foot pager" aria-label="Pagination">
|
||||||
@if ($agents->onFirstPage())
|
@if ($agents->onFirstPage())
|
||||||
<span class="muted">← Précédent</span>
|
<span class="btn secondary small" aria-disabled="true"><x-icon name="chevron-left" :size="15" />Précédent</span>
|
||||||
@else
|
@else
|
||||||
<a href="{{ $agents->previousPageUrl() }}" rel="prev">← Précédent</a>
|
<a class="btn secondary small" href="{{ $agents->previousPageUrl() }}" rel="prev"><x-icon name="chevron-left" :size="15" />Précédent</a>
|
||||||
@endif
|
@endif
|
||||||
<span class="muted">Page {{ $agents->currentPage() }} / {{ $agents->lastPage() }}</span>
|
<span class="muted">Page {{ $agents->currentPage() }} sur {{ $agents->lastPage() }}</span>
|
||||||
@if ($agents->hasMorePages())
|
@if ($agents->hasMorePages())
|
||||||
<a href="{{ $agents->nextPageUrl() }}" rel="next">Suivant →</a>
|
<a class="btn secondary small" href="{{ $agents->nextPageUrl() }}" rel="next">Suivant<x-icon name="chevron-right" :size="15" /></a>
|
||||||
@else
|
@else
|
||||||
<span class="muted">Suivant →</span>
|
<span class="btn secondary small" aria-disabled="true">Suivant<x-icon name="chevron-right" :size="15" /></span>
|
||||||
@endif
|
@endif
|
||||||
</nav>
|
</nav>
|
||||||
@endif
|
@endif
|
||||||
|
|||||||
@@ -1,41 +1,51 @@
|
|||||||
@extends('layouts.app')
|
@extends('layouts.app')
|
||||||
@section('title', $agent->display_name.' — Signatures CABM')
|
@section('title', $agent->display_name.' — Signatures')
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="row" style="margin-bottom:16px">
|
<header class="page-head">
|
||||||
<a href="{{ route('admin.agents.index') }}">← Agents</a>
|
<div>
|
||||||
<h1 style="margin:0 0 0 12px">{{ $agent->display_name }}</h1>
|
<a class="back" href="{{ route('admin.agents.index') }}"><x-icon name="chevron-left" :size="16" />Agents</a>
|
||||||
|
<h1>{{ $agent->display_name }}</h1>
|
||||||
|
<p class="lead">{{ collect([$agent->job_title, $agent->department])->filter()->join(', ') }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
<div class="split">
|
<div class="split">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Annuaire (Entra)</h2>
|
<h2>Annuaire (Entra)</h2>
|
||||||
<table class="list">
|
<dl class="facts">
|
||||||
<tr><th>Adresse</th><td>{{ $agent->mail }}</td></tr>
|
<dt>Adresse</dt><dd>{{ $agent->mail ?: '—' }}</dd>
|
||||||
<tr><th>Fonction</th><td>{{ $agent->job_title }}</td></tr>
|
<dt>Fonction</dt><dd>{{ $agent->job_title ?: '—' }}</dd>
|
||||||
<tr><th>Service</th><td>{{ $agent->department }}</td></tr>
|
<dt>Service</dt><dd>{{ $agent->department ?: '—' }}</dd>
|
||||||
<tr><th>Téléphone</th><td>{{ $agent->business_phone }}</td></tr>
|
<dt>Téléphone</dt><dd>{{ $agent->business_phone ?: '—' }}</dd>
|
||||||
<tr><th>Mobile</th><td>{{ $agent->mobile_phone }}</td></tr>
|
<dt>Mobile</dt><dd>{{ $agent->mobile_phone ?: '—' }}</dd>
|
||||||
<tr><th>Groupes signature_*</th><td>{{ $agent->groups->pluck('display_name')->join(', ') ?: '—' }}</td></tr>
|
<dt>Groupes signature_*</dt><dd>{{ $agent->groups->pluck('display_name')->join(', ') ?: '—' }}</dd>
|
||||||
<tr><th>Template appliqué</th><td>
|
<dt>Template appliqué</dt>
|
||||||
|
<dd>
|
||||||
@if ($template)
|
@if ($template)
|
||||||
<a href="{{ route('admin.templates.edit', $template) }}">{{ $template->name }}</a>
|
<a href="{{ route('admin.templates.edit', $template) }}">{{ $template->name }}</a>
|
||||||
@else — aucun (pas de template par défaut publié) — @endif
|
@else
|
||||||
</td></tr>
|
<span class="badge warn">aucun</span> <span class="muted">pas de template par défaut publié</span>
|
||||||
<tr><th>Bannière</th><td>
|
@endif
|
||||||
|
</dd>
|
||||||
|
<dt>Bannière</dt>
|
||||||
|
<dd>
|
||||||
@if ($campaign)
|
@if ($campaign)
|
||||||
<a href="{{ route('admin.campaigns.edit', $campaign) }}">{{ $campaign->name }}</a>
|
<a href="{{ route('admin.campaigns.edit', $campaign) }}">{{ $campaign->name }}</a>
|
||||||
@if ($template && ! str_contains((string) $template->html_full, 'banniere'))
|
@if ($template && ! str_contains((string) $template->html_full, 'banniere'))
|
||||||
<span class="badge warn">non affichée : le template n'a pas de bannière</span>
|
<span class="badge warn">non affichée : le template n'a pas de bannière</span>
|
||||||
@endif
|
@endif
|
||||||
@else — aucune campagne en cours pour cet agent — @endif
|
@else
|
||||||
</td></tr>
|
<span class="muted">Aucune campagne en cours pour cet agent</span>
|
||||||
<tr><th>Synchronisé le</th><td class="muted">{{ $agent->synced_at?->timezone('Europe/Paris')->format('d/m/Y H:i') }}</td></tr>
|
@endif
|
||||||
</table>
|
</dd>
|
||||||
|
<dt>Synchronisé le</dt><dd class="muted">{{ $agent->synced_at?->timezone('Europe/Paris')->format('d/m/Y H:i') ?? '—' }}</dd>
|
||||||
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Nouveau message</h2>
|
<h2>Nouveau message</h2>
|
||||||
@include('partials.signature-frame', ['html' => $full['html'] ?? ''])
|
@include('partials.signature-frame', ['html' => $full['html'] ?? ''])
|
||||||
<h2 style="margin-top:16px">Réponse / transfert</h2>
|
<h2 style="margin-top:18px">Réponse et transfert</h2>
|
||||||
@include('partials.signature-frame', ['html' => $reply['html'] ?? ''])
|
@include('partials.signature-frame', ['html' => $reply['html'] ?? '', 'class' => 'short'])
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
@@ -1,79 +1,92 @@
|
|||||||
@extends('layouts.app')
|
@extends('layouts.app')
|
||||||
@section('title', 'Affectations — Signatures CABM')
|
@section('title', 'Affectations — Signatures')
|
||||||
@section('content')
|
@section('content')
|
||||||
|
<header class="page-head">
|
||||||
|
<div>
|
||||||
<h1>Affectations</h1>
|
<h1>Affectations</h1>
|
||||||
<p class="muted" style="margin-top:-8px">
|
<p class="lead">
|
||||||
Un agent membre de plusieurs groupes reçoit le template de l'affectation dont la <strong>priorité est la plus haute</strong>
|
Un agent membre de plusieurs groupes reçoit le template de l'affectation dont la <strong>priorité est la plus haute</strong>
|
||||||
(à égalité : la plus ancienne). Sans groupe affecté, il reçoit le template par défaut.
|
(à égalité : la plus ancienne). Sans groupe affecté, il reçoit le template par défaut.
|
||||||
Tous les groupes AD synchronisés sont proposés, sauf les groupes de rôles du back-office.
|
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
<div class="card">
|
<form method="post" action="{{ route('admin.assignments.store') }}" class="card">
|
||||||
<h2>Ajouter une affectation</h2>
|
|
||||||
<form method="post" action="{{ route('admin.assignments.store') }}" class="row" style="align-items:flex-end">
|
|
||||||
@csrf
|
@csrf
|
||||||
<div style="flex:2;min-width:220px">
|
<h2>Ajouter une affectation</h2>
|
||||||
<label for="directory_group_id">Groupe AD</label>
|
<div class="field-row" style="align-items:flex-end">
|
||||||
|
<div style="flex:2">
|
||||||
|
<label for="directory_group_id" style="margin-top:0">Groupe AD</label>
|
||||||
<select id="directory_group_id" name="directory_group_id" required>
|
<select id="directory_group_id" name="directory_group_id" required>
|
||||||
<option value="">— choisir —</option>
|
<option value="">Choisir un groupe</option>
|
||||||
@if ($groups->isEmpty())
|
@if ($groups->isEmpty())
|
||||||
<option value="" disabled>Aucun groupe synchronisé : lancez php artisan signature:sync --full</option>
|
<option value="" disabled>Aucun groupe synchronisé : lancez une synchronisation complète</option>
|
||||||
@endif
|
@endif
|
||||||
@foreach ($groups as $g)
|
@foreach ($groups as $g)
|
||||||
<option value="{{ $g->id }}" @selected(old('directory_group_id') == $g->id)>{{ $g->display_name }} ({{ $g->members_count }})</option>
|
<option value="{{ $g->id }}" @selected(old('directory_group_id') == $g->id)>{{ $g->display_name }} ({{ $g->members_count }})</option>
|
||||||
@endforeach
|
@endforeach
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div style="flex:2;min-width:220px">
|
<div style="flex:2">
|
||||||
<label for="template_id">Template</label>
|
<label for="template_id" style="margin-top:0">Template</label>
|
||||||
<select id="template_id" name="template_id" required>
|
<select id="template_id" name="template_id" required>
|
||||||
<option value="">— choisir —</option>
|
<option value="">Choisir un template</option>
|
||||||
@foreach ($templates as $t)
|
@foreach ($templates as $t)
|
||||||
<option value="{{ $t->id }}" @selected(old('template_id') == $t->id)>{{ $t->name }}{{ $t->html_full ? '' : ' (non publié)' }}</option>
|
<option value="{{ $t->id }}" @selected(old('template_id') == $t->id)>{{ $t->name }}{{ $t->html_full ? '' : ' (non publié)' }}</option>
|
||||||
@endforeach
|
@endforeach
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div style="flex:0 0 96px;min-width:96px">
|
||||||
<label for="priority">Priorité</label>
|
<label for="priority" style="margin-top:0">Priorité</label>
|
||||||
<input class="small" type="number" id="priority" name="priority" min="0" max="1000" value="{{ old('priority', 10) }}" required>
|
<input type="number" id="priority" name="priority" min="0" max="1000" value="{{ old('priority', 10) }}" required>
|
||||||
|
</div>
|
||||||
|
<div style="flex:0 0 auto;min-width:0">
|
||||||
|
<button class="btn" type="submit"><x-icon name="plus" />Ajouter</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn" type="submit">Ajouter</button>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
<div class="card card--flush">
|
||||||
<h2>Affectations actives, par ordre de priorité</h2>
|
<div class="card-head"><h2>Affectations, par ordre de priorité</h2></div>
|
||||||
|
<div class="table-wrap">
|
||||||
<table class="list">
|
<table class="list">
|
||||||
<thead><tr><th>Priorité</th><th>Groupe AD</th><th>Membres</th><th>Template</th><th></th></tr></thead>
|
<thead><tr><th>Priorité</th><th>Groupe AD</th><th>Membres</th><th>Template</th><th></th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@forelse ($assignments as $a)
|
@forelse ($assignments as $a)
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<form class="row" method="post" action="{{ route('admin.assignments.update', $a) }}">
|
<form class="row" method="post" action="{{ route('admin.assignments.update', $a) }}" style="flex-wrap:nowrap">
|
||||||
@csrf @method('PUT')
|
@csrf @method('PUT')
|
||||||
<input class="small" type="number" name="priority" min="0" max="1000" value="{{ $a->priority }}" aria-label="Priorité">
|
<input class="small" type="number" name="priority" min="0" max="1000" value="{{ $a->priority }}" aria-label="Priorité de {{ $a->group?->display_name }}">
|
||||||
<button class="btn secondary small" type="submit">OK</button>
|
<button class="btn secondary small" type="submit" title="Enregistrer la priorité" aria-label="Enregistrer la priorité"><x-icon name="check" :size="15" /></button>
|
||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ $a->group?->display_name }}</td>
|
<td>{{ $a->group?->display_name }}</td>
|
||||||
<td>{{ $a->group?->members_count }}</td>
|
<td class="num">{{ $a->group?->members_count }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ route('admin.templates.edit', $a->template_id) }}">{{ $a->template?->name }}</a>
|
<a class="cell-title" href="{{ route('admin.templates.edit', $a->template_id) }}">{{ $a->template?->name }}</a>
|
||||||
@unless ($a->template?->html_full) <span class="badge warn">non publié : ignoré</span> @endunless
|
@unless ($a->template?->html_full) <span class="badge warn">non publié : ignoré</span> @endunless
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align:right">
|
<td class="cell-actions">
|
||||||
<form class="inline" method="post" action="{{ route('admin.assignments.destroy', $a) }}"
|
<form class="inline" method="post" action="{{ route('admin.assignments.destroy', $a) }}"
|
||||||
onsubmit="return confirm('Supprimer cette affectation ?')">
|
data-confirm="Supprimer cette affectation ? Les agents du groupe recevront le template suivant dans l'ordre de priorité, ou le template par défaut."
|
||||||
|
data-confirm-label="Supprimer" data-confirm-danger>
|
||||||
@csrf @method('DELETE')
|
@csrf @method('DELETE')
|
||||||
<button class="btn danger small" type="submit">Supprimer</button>
|
<button class="btn danger small" type="submit"><x-icon name="trash-2" :size="15" />Supprimer</button>
|
||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@empty
|
@empty
|
||||||
<tr><td colspan="5" class="muted">Aucune affectation : tous les agents reçoivent le template par défaut.</td></tr>
|
<tr><td colspan="5">
|
||||||
|
<div class="empty">
|
||||||
|
<x-icon name="users" :size="32" />
|
||||||
|
<p><strong>Aucune affectation</strong>Tous les agents reçoivent le template par défaut.</p>
|
||||||
|
</div>
|
||||||
|
</td></tr>
|
||||||
@endforelse
|
@endforelse
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<p class="muted">Vérifiez le résultat agent par agent dans l'onglet <a href="{{ route('admin.agents.index') }}">Agents</a>.</p>
|
</div>
|
||||||
|
<div class="card-foot muted">Vérifiez le résultat agent par agent dans l'écran <a href="{{ route('admin.agents.index') }}">Agents</a>.</div>
|
||||||
</div>
|
</div>
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@extends('layouts.app')
|
@extends('layouts.app')
|
||||||
@section('title', ($campaign->exists ? $campaign->name : 'Nouvelle campagne').' — Signatures CABM')
|
@section('title', ($campaign->exists ? $campaign->name : 'Nouvelle campagne').' — Signatures')
|
||||||
@section('content')
|
@section('content')
|
||||||
@php
|
@php
|
||||||
$isAdmin = auth()->user()->hasRole('admin');
|
$isAdmin = auth()->user()->hasRole('admin');
|
||||||
@@ -11,27 +11,34 @@
|
|||||||
$checked = collect(old('groups', $selected))->map(fn ($id) => (int) $id)->all();
|
$checked = collect(old('groups', $selected))->map(fn ($id) => (int) $id)->all();
|
||||||
$status = $campaign->exists ? $campaign->status() : null;
|
$status = $campaign->exists ? $campaign->status() : null;
|
||||||
@endphp
|
@endphp
|
||||||
<div class="row" style="margin-bottom:16px">
|
<header class="page-head">
|
||||||
<a href="{{ route('admin.campaigns.index') }}">← Campagnes</a>
|
<div>
|
||||||
<h1 style="margin:0 0 0 12px">{{ $campaign->exists ? $campaign->name : 'Nouvelle campagne' }}</h1>
|
<a class="back" href="{{ route('admin.campaigns.index') }}"><x-icon name="chevron-left" :size="16" />Campagnes</a>
|
||||||
|
<div class="page-title">
|
||||||
|
<h1>{{ $campaign->exists ? $campaign->name : 'Nouvelle campagne' }}</h1>
|
||||||
@if ($status)
|
@if ($status)
|
||||||
<span class="badge {{ ['running' => 'ok', 'inactive' => 'warn'][$status] ?? '' }}">{{ \App\Models\Campaign::STATUSES[$status] }}</span>
|
<span class="badge {{ ['running' => 'ok', 'inactive' => 'warn', 'ended' => 'off'][$status] ?? '' }}">{{ \App\Models\Campaign::STATUSES[$status] }}</span>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
@if ($locked)
|
@if ($locked)
|
||||||
<div class="notice" role="note">Cette campagne est active : seul un administrateur peut la modifier.</div>
|
<div class="alert notice" role="note">
|
||||||
|
<x-icon name="info" />
|
||||||
|
<div>Cette campagne est active : seul un administrateur peut la modifier.</div>
|
||||||
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<form method="post" enctype="multipart/form-data"
|
<form method="post" enctype="multipart/form-data"
|
||||||
action="{{ $campaign->exists ? route('admin.campaigns.update', $campaign) : route('admin.campaigns.store') }}">
|
action="{{ $campaign->exists ? route('admin.campaigns.update', $campaign) : route('admin.campaigns.store') }}">
|
||||||
@csrf
|
@csrf
|
||||||
@if ($campaign->exists) @method('PUT') @endif
|
@if ($campaign->exists) @method('PUT') @endif
|
||||||
<fieldset @disabled($locked) style="border:0;margin:0;padding:0">
|
<fieldset class="bare" @disabled($locked)>
|
||||||
<div class="split">
|
<div class="split">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Bannière</h2>
|
<h2>Bannière</h2>
|
||||||
<label for="name">Nom de la campagne</label>
|
<label for="name" style="margin-top:0">Nom de la campagne</label>
|
||||||
<input type="text" id="name" name="name" maxlength="120" required value="{{ old('name', $campaign->name) }}" placeholder="ex. Collecte des encombrants — octobre">
|
<input type="text" id="name" name="name" maxlength="120" required value="{{ old('name', $campaign->name) }}" placeholder="ex. Collecte des encombrants — octobre">
|
||||||
|
|
||||||
<label for="image_path">Image</label>
|
<label for="image_path">Image</label>
|
||||||
@@ -46,40 +53,45 @@
|
|||||||
</option>
|
</option>
|
||||||
@endforeach
|
@endforeach
|
||||||
</select>
|
</select>
|
||||||
<label for="image_file">… ou envoyer une nouvelle image (PNG, JPEG, GIF)</label>
|
<label class="dropzone" data-dropzone style="margin-top:10px">
|
||||||
<input type="file" id="image_file" name="image_file" accept="image/png,image/jpeg,image/gif">
|
<input type="file" id="image_file" name="image_file" accept="image/png,image/jpeg,image/gif" aria-label="Nouvelle image">
|
||||||
<p class="muted">Visez 600 px de large et moins de 100 Ko : les destinataires la téléchargent à chaque ouverture du message.</p>
|
<x-icon name="cloud-upload" :size="24" />
|
||||||
<img id="thumb" alt="" hidden style="max-width:100%;max-height:160px;border:1px solid var(--line);background:#fff">
|
<span data-prompt>… ou glissez une nouvelle image ici, ou <u>parcourez vos fichiers</u></span>
|
||||||
|
<span class="dropzone-name" data-name hidden></span>
|
||||||
|
<span class="muted">PNG, JPEG ou GIF</span>
|
||||||
|
</label>
|
||||||
|
<p class="hint">Visez 600 px de large et moins de 100 Ko : les destinataires la téléchargent à chaque ouverture du message.</p>
|
||||||
|
<img id="thumb" class="preview-img" alt="" hidden>
|
||||||
|
|
||||||
<label for="alt_text">Texte alternatif</label>
|
<label for="alt_text">Texte alternatif</label>
|
||||||
<input type="text" id="alt_text" name="alt_text" maxlength="150" required value="{{ old('alt_text', $campaign->alt_text) }}" placeholder="ex. Collecte des encombrants : inscrivez-vous avant le 15 octobre">
|
<input type="text" id="alt_text" name="alt_text" maxlength="150" required value="{{ old('alt_text', $campaign->alt_text) }}" placeholder="ex. Collecte des encombrants : inscrivez-vous avant le 15 octobre">
|
||||||
<p class="muted">Affiché quand le destinataire bloque les images, et lu par les lecteurs d'écran. Résumez le message de la bannière.</p>
|
<p class="hint">Affiché quand le destinataire bloque les images, et lu par les lecteurs d'écran. Résumez le message de la bannière.</p>
|
||||||
|
|
||||||
<label for="link_url">Lien au clic (facultatif)</label>
|
<label for="link_url">Lien au clic (facultatif)</label>
|
||||||
<input type="url" id="link_url" name="link_url" maxlength="255" value="{{ old('link_url', $campaign->link_url) }}" placeholder="https://www.beziers-mediterranee.fr/…">
|
<input type="url" id="link_url" name="link_url" maxlength="255" value="{{ old('link_url', $campaign->link_url) }}" placeholder="https://www.beziers-mediterranee.fr/…">
|
||||||
|
|
||||||
<label for="width">Largeur d'affichage (px, facultatif)</label>
|
<label for="width">Largeur d'affichage (px, facultatif)</label>
|
||||||
<input type="number" id="width" name="width" style="width:140px" min="100" max="800" value="{{ old('width', $width) }}" placeholder="auto">
|
<input type="number" id="width" name="width" style="width:140px" min="100" max="800" value="{{ old('width', $width) }}" placeholder="auto">
|
||||||
<p class="muted">Vide : largeur réelle de l'image, plafonnée à 600 px.</p>
|
<p class="hint">Vide : largeur réelle de l'image, plafonnée à 600 px.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Diffusion</h2>
|
<h2>Diffusion</h2>
|
||||||
<div class="row">
|
<div class="field-row">
|
||||||
<div style="flex:1;min-width:200px">
|
<div>
|
||||||
<label for="starts_at">Début</label>
|
<label for="starts_at" style="margin-top:0">Début</label>
|
||||||
<input type="datetime-local" id="starts_at" name="starts_at" required value="{{ old('starts_at', $local($campaign->starts_at)) }}">
|
<input type="datetime-local" id="starts_at" name="starts_at" required value="{{ old('starts_at', $local($campaign->starts_at)) }}">
|
||||||
</div>
|
</div>
|
||||||
<div style="flex:1;min-width:200px">
|
<div>
|
||||||
<label for="ends_at">Fin</label>
|
<label for="ends_at" style="margin-top:0">Fin</label>
|
||||||
<input type="datetime-local" id="ends_at" name="ends_at" required value="{{ old('ends_at', $local($campaign->ends_at)) }}">
|
<input type="datetime-local" id="ends_at" name="ends_at" required value="{{ old('ends_at', $local($campaign->ends_at)) }}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="muted">Heure de Paris. La bannière cesse d'être insérée à la fin ; les messages déjà envoyés la conservent.</p>
|
<p class="hint">Heure de Paris. La bannière cesse d'être insérée à la fin ; les messages déjà envoyés la conservent.</p>
|
||||||
|
|
||||||
<label for="priority">Priorité</label>
|
<label for="priority">Priorité</label>
|
||||||
<input class="small" type="number" id="priority" name="priority" min="0" max="1000" required value="{{ old('priority', $campaign->priority ?? 10) }}">
|
<input class="small" type="number" id="priority" name="priority" min="0" max="1000" required value="{{ old('priority', $campaign->priority ?? 10) }}">
|
||||||
<p class="muted">Si plusieurs campagnes en cours concernent le même agent, la priorité la plus haute s'affiche.</p>
|
<p class="hint">Si plusieurs campagnes en cours concernent le même agent, la priorité la plus haute s'affiche.</p>
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Destinataires</legend>
|
<legend>Destinataires</legend>
|
||||||
@@ -98,18 +110,19 @@
|
|||||||
|
|
||||||
@if ($isAdmin)
|
@if ($isAdmin)
|
||||||
<input type="hidden" name="active" value="0">
|
<input type="hidden" name="active" value="0">
|
||||||
<label class="check" style="margin-top:14px">
|
<label class="switch" style="margin-top:16px">
|
||||||
<input type="checkbox" name="active" value="1" @checked(old('active', $campaign->active))>Campagne active
|
<input type="checkbox" name="active" value="1" @checked(old('active', $campaign->active))>
|
||||||
|
<span class="switch-track"></span><strong>Campagne active</strong>
|
||||||
</label>
|
</label>
|
||||||
<p class="muted">Décochée, la bannière n'est jamais insérée, même pendant la période.</p>
|
<p class="hint">Désactivée, la bannière n'est jamais insérée, même pendant la période.</p>
|
||||||
@else
|
@else
|
||||||
<p class="muted" style="margin-top:14px">La campagne reste inactive tant qu'un administrateur ne l'a pas activée.</p>
|
<p class="hint" style="margin-top:14px">La campagne reste inactive tant qu'un administrateur ne l'a pas activée.</p>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<button class="btn" type="submit">{{ $campaign->exists ? 'Enregistrer' : 'Créer la campagne' }}</button>
|
<button class="btn" type="submit"><x-icon name="save" />{{ $campaign->exists ? 'Enregistrer' : 'Créer la campagne' }}</button>
|
||||||
<a class="btn secondary" href="{{ route('admin.campaigns.index') }}">Annuler</a>
|
<a class="btn secondary" href="{{ route('admin.campaigns.index') }}">Annuler</a>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@@ -117,9 +130,9 @@
|
|||||||
|
|
||||||
@if ($campaign->exists)
|
@if ($campaign->exists)
|
||||||
<div class="card" style="margin-top:16px">
|
<div class="card" style="margin-top:16px">
|
||||||
<h2>Rendu enregistré</h2>
|
<h2>Bannière enregistrée</h2>
|
||||||
<p class="muted">Tel qu'inséré à l'emplacement <code>{{ '{'.'{'.'{banniere}'.'}'.'}' }}</code> du template. Pour le voir dans une signature complète, ouvrez un agent dans l'onglet <a href="{{ route('admin.agents.index') }}">Agents</a>.</p>
|
<p class="muted">Tel qu'inséré à l'emplacement <code>{{ '{'.'{'.'{banniere}'.'}'.'}' }}</code> du template. Pour le voir dans une signature complète, ouvrez un agent dans l'onglet <a href="{{ route('admin.agents.index') }}">Agents</a>.</p>
|
||||||
@include('partials.signature-frame', ['html' => $bannerHtml, 'title' => 'Aperçu de la bannière'])
|
@include('partials.signature-frame', ['html' => $bannerHtml, 'title' => 'Aperçu de la bannière', 'class' => 'short'])
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
@endsection
|
@endsection
|
||||||
@@ -140,7 +153,7 @@
|
|||||||
width.placeholder = natural ? `auto (${Math.min(natural, 600)})` : 'auto';
|
width.placeholder = natural ? `auto (${Math.min(natural, 600)})` : 'auto';
|
||||||
};
|
};
|
||||||
|
|
||||||
select.addEventListener('change', () => { file.value = ''; fromLibrary(); });
|
select.addEventListener('change', () => { file.value = ''; file.dispatchEvent(new Event('change')); });
|
||||||
file.addEventListener('change', () => {
|
file.addEventListener('change', () => {
|
||||||
if (!file.files.length) return fromLibrary();
|
if (!file.files.length) return fromLibrary();
|
||||||
select.value = '';
|
select.value = '';
|
||||||
|
|||||||
@@ -1,36 +1,41 @@
|
|||||||
@extends('layouts.app')
|
@extends('layouts.app')
|
||||||
@section('title', 'Campagnes — Signatures CABM')
|
@section('title', 'Campagnes — Signatures')
|
||||||
@section('content')
|
@section('content')
|
||||||
@php
|
@php
|
||||||
$isAdmin = auth()->user()->hasRole('admin');
|
$isAdmin = auth()->user()->hasRole('admin');
|
||||||
$tz = config('signature.timezone');
|
$tz = config('signature.timezone');
|
||||||
$badge = ['running' => 'ok', 'scheduled' => '', 'inactive' => 'warn', 'ended' => ''];
|
$badge = ['running' => 'ok', 'scheduled' => '', 'inactive' => 'warn', 'ended' => 'off'];
|
||||||
$variable = '{'.'{'.'{banniere}'.'}'.'}';
|
$variable = '{'.'{'.'{banniere}'.'}'.'}';
|
||||||
$period = fn ($d) => $d->timezone($tz)->format('d/m/Y H:i');
|
$period = fn ($d) => $d->timezone($tz)->format('d/m/Y H:i');
|
||||||
@endphp
|
@endphp
|
||||||
<div class="row" style="margin-bottom:8px">
|
<header class="page-head">
|
||||||
<h1 style="margin:0">Campagnes</h1>
|
<div>
|
||||||
<span class="spacer"></span>
|
<h1>Campagnes</h1>
|
||||||
<a class="btn" href="{{ route('admin.campaigns.create') }}">Nouvelle campagne</a>
|
<p class="lead">
|
||||||
</div>
|
|
||||||
<p class="muted">
|
|
||||||
Une campagne ajoute une bannière sous la signature pendant une période, pour tous les agents ou pour certains groupes AD.
|
Une campagne ajoute une bannière sous la signature pendant une période, pour tous les agents ou pour certains groupes AD.
|
||||||
<strong>Une seule bannière par signature</strong> : si plusieurs campagnes en cours concernent un agent, celle dont la priorité
|
<strong>Une seule bannière par signature</strong> : si plusieurs campagnes en cours concernent un agent, celle dont la priorité
|
||||||
est la plus haute l'emporte (à égalité : la plus ancienne). La bannière n'apparaît que dans la signature complète,
|
est la plus haute l'emporte (à égalité : la plus ancienne). La bannière n'apparaît que dans la signature complète,
|
||||||
pas dans la signature courte des réponses, et seulement si le template contient <code>{{ $variable }}</code>.
|
pas dans la signature courte des réponses.
|
||||||
@unless ($isAdmin) Vous préparez les campagnes ; un administrateur les active. @endunless
|
@unless ($isAdmin) Vous préparez les campagnes ; un administrateur les active. @endunless
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="page-actions">
|
||||||
|
<a class="btn" href="{{ route('admin.campaigns.create') }}"><x-icon name="plus" />Nouvelle campagne</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
@if ($withoutBanner->isNotEmpty())
|
@if ($withoutBanner->isNotEmpty())
|
||||||
<div class="notice" role="note">
|
<div class="alert notice" role="note">
|
||||||
Ces templates publiés ne contiennent pas <code>{{ $variable }}</code> : leurs agents ne verront aucune bannière —
|
<x-icon name="triangle-alert" />
|
||||||
{{ $withoutBanner->join(', ') }}.
|
<div>Ces templates publiés ne contiennent pas <code>{{ $variable }}</code> : leurs agents ne verront aucune bannière.
|
||||||
|
<strong>{{ $withoutBanner->join(', ') }}</strong></div>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<div class="card" style="overflow-x:auto">
|
<div class="card card--flush">
|
||||||
|
<div class="table-wrap">
|
||||||
<table class="list">
|
<table class="list">
|
||||||
<thead><tr><th style="width:130px">Bannière</th><th>Campagne</th><th>Période</th><th>Destinataires</th><th>Priorité</th><th>État</th><th></th></tr></thead>
|
<thead><tr><th>Bannière</th><th>Campagne</th><th>Période</th><th>Destinataires</th><th>Priorité</th><th>État</th><th class="right">{{ $isAdmin ? 'Active' : '' }}</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@forelse ($campaigns as $c)
|
@forelse ($campaigns as $c)
|
||||||
@php
|
@php
|
||||||
@@ -43,22 +48,22 @@
|
|||||||
<span class="badge warn">image manquante</span>
|
<span class="badge warn">image manquante</span>
|
||||||
<div class="muted">{{ $c->image_path }}</div>
|
<div class="muted">{{ $c->image_path }}</div>
|
||||||
@else
|
@else
|
||||||
<img src="{{ asset('img/'.$c->image_path) }}" alt="{{ $c->alt_text }}" style="max-width:120px;max-height:60px;border:1px solid var(--line);background:#fff">
|
<img class="thumb" src="{{ asset('img/'.$c->image_path) }}" alt="{{ $c->alt_text }}" loading="lazy">
|
||||||
@endif
|
@endif
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ route('admin.campaigns.edit', $c) }}"><strong>{{ $c->name }}</strong></a>
|
<a class="cell-title" href="{{ route('admin.campaigns.edit', $c) }}">{{ $c->name }}</a>
|
||||||
<div class="muted">
|
<div class="muted">
|
||||||
@if ($c->link_url)
|
@if ($c->link_url)
|
||||||
Lien : <a href="{{ $c->link_url }}" target="_blank" rel="noopener">{{ \Illuminate\Support\Str::limit($c->link_url, 50) }}</a>
|
<a href="{{ $c->link_url }}" target="_blank" rel="noopener" title="{{ $c->link_url }}">{{ \Illuminate\Support\Str::limit(preg_replace('#^https?://(www\.)?#', '', $c->link_url), 38) }}</a>
|
||||||
@else
|
@else
|
||||||
Sans lien
|
Sans lien
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td style="white-space:nowrap">{{ $period($c->starts_at) }}<br>→ {{ $period($c->ends_at) }}</td>
|
<td class="nowrap">{{ $period($c->starts_at) }}<br><span class="muted">au</span> {{ $period($c->ends_at) }}</td>
|
||||||
<td style="overflow-wrap:anywhere">{{ $c->groups->isEmpty() ? 'Tous les agents' : $c->groups->pluck('display_name')->join(', ') }}</td>
|
<td style="min-width:130px;overflow-wrap:anywhere">{{ $c->groups->isEmpty() ? 'Tous les agents' : $c->groups->pluck('display_name')->join(', ') }}</td>
|
||||||
<td>{{ $c->priority }}</td>
|
<td class="num">{{ $c->priority }}</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge {{ $badge[$status] }}">{{ \App\Models\Campaign::STATUSES[$status] }}</span>
|
<span class="badge {{ $badge[$status] }}">{{ \App\Models\Campaign::STATUSES[$status] }}</span>
|
||||||
@if ($status === 'running')
|
@if ($status === 'running')
|
||||||
@@ -73,28 +78,40 @@
|
|||||||
<div class="muted">{{ $isAdmin ? 'à activer' : 'en attente d\'un administrateur' }}</div>
|
<div class="muted">{{ $isAdmin ? 'à activer' : 'en attente d\'un administrateur' }}</div>
|
||||||
@endif
|
@endif
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align:right">
|
<td class="cell-actions">
|
||||||
<a class="btn secondary small" style="margin:2px 0" href="{{ route('admin.campaigns.edit', $c) }}">{{ $locked ? 'Voir' : 'Modifier' }}</a>
|
<a class="btn secondary small icon-only" href="{{ route('admin.campaigns.edit', $c) }}"
|
||||||
|
title="{{ $locked ? 'Voir' : 'Modifier' }}" aria-label="{{ $locked ? 'Voir' : 'Modifier' }} {{ $c->name }}">
|
||||||
|
<x-icon :name="$locked ? 'eye' : 'pencil'" :size="15" />
|
||||||
|
</a>
|
||||||
@if ($isAdmin)
|
@if ($isAdmin)
|
||||||
@if ($status !== 'ended')
|
@if ($status !== 'ended')
|
||||||
<form class="inline" method="post" action="{{ route('admin.campaigns.toggle', $c) }}">
|
<form class="inline" method="post" action="{{ route('admin.campaigns.toggle', $c) }}">
|
||||||
@csrf
|
@csrf
|
||||||
<button class="btn secondary small" style="margin:2px 0" type="submit">{{ $c->active ? 'Désactiver' : 'Activer' }}</button>
|
<button class="switch-btn" type="submit" role="switch" aria-checked="{{ $c->active ? 'true' : 'false' }}"
|
||||||
|
title="{{ $c->active ? 'Désactiver' : 'Activer' }}" aria-label="Campagne {{ $c->name }} active"></button>
|
||||||
</form>
|
</form>
|
||||||
@endif
|
@endif
|
||||||
<form class="inline" method="post" action="{{ route('admin.campaigns.destroy', $c) }}"
|
<form class="inline" method="post" action="{{ route('admin.campaigns.destroy', $c) }}"
|
||||||
onsubmit="return confirm('Supprimer cette campagne ? Les messages déjà envoyés conservent la bannière.')">
|
data-confirm="Supprimer la campagne « {{ $c->name }} » ? Les messages déjà envoyés conservent la bannière."
|
||||||
|
data-confirm-label="Supprimer" data-confirm-danger>
|
||||||
@csrf @method('DELETE')
|
@csrf @method('DELETE')
|
||||||
<button class="btn danger small" style="margin:2px 0" type="submit">Supprimer</button>
|
<button class="btn danger small icon-only" type="submit" title="Supprimer" aria-label="Supprimer {{ $c->name }}"><x-icon name="trash-2" :size="15" /></button>
|
||||||
</form>
|
</form>
|
||||||
@endif
|
@endif
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@empty
|
@empty
|
||||||
<tr><td colspan="7" class="muted">Aucune campagne. Les signatures sont envoyées sans bannière.</td></tr>
|
<tr><td colspan="7">
|
||||||
|
<div class="empty">
|
||||||
|
<x-icon name="megaphone" :size="32" />
|
||||||
|
<p><strong>Aucune campagne</strong>Les signatures partent sans bannière. Créez une campagne pour en afficher une pendant une période donnée.</p>
|
||||||
|
<p><a class="btn" href="{{ route('admin.campaigns.create') }}"><x-icon name="plus" />Nouvelle campagne</a></p>
|
||||||
|
</div>
|
||||||
|
</td></tr>
|
||||||
@endforelse
|
@endforelse
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<p class="muted">Vérifiez la bannière reçue par un agent précis dans l'onglet <a href="{{ route('admin.agents.index') }}">Agents</a>.</p>
|
</div>
|
||||||
|
<div class="card-foot muted">Vérifiez la bannière reçue par un agent précis dans l'écran <a href="{{ route('admin.agents.index') }}">Agents</a>.</div>
|
||||||
</div>
|
</div>
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
@@ -1,55 +1,85 @@
|
|||||||
@extends('layouts.app')
|
@extends('layouts.app')
|
||||||
@section('title', 'Back-office — Signatures CABM')
|
@section('title', 'Tableau de bord — Signatures')
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="card">
|
@php
|
||||||
<h1>{{ $tenant->name }}</h1>
|
$isAdmin = auth()->user()->hasRole('admin');
|
||||||
<div class="row" style="margin-bottom:12px">
|
$syncOk = $tenant->last_sync_status === 'ok';
|
||||||
<p class="muted" style="margin:0">
|
@endphp
|
||||||
Dernière synchro annuaire :
|
<header class="page-head">
|
||||||
{{ $tenant->last_sync_at?->timezone('Europe/Paris')->format('d/m/Y H:i') ?? 'jamais' }}
|
<div>
|
||||||
— {{ $tenant->last_sync_status ?? 'n/a' }}
|
<h1>Tableau de bord</h1>
|
||||||
<br>Automatique toutes les 15 minutes (incrémentale) et chaque nuit (complète).
|
<p class="lead">{{ $tenant->name }}</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="card sync">
|
||||||
|
<span @class(['sync-dot', 'warn' => ! $syncOk]) aria-hidden="true"></span>
|
||||||
|
<div>
|
||||||
|
<p><strong>Annuaire synchronisé
|
||||||
|
{{ $tenant->last_sync_at ? 'le '.$tenant->last_sync_at->timezone('Europe/Paris')->format('d/m/Y à H:i') : ': jamais' }}</strong>
|
||||||
|
@if ($tenant->last_sync_at && ! $syncOk) <span class="badge warn">{{ $tenant->last_sync_status ?? 'état inconnu' }}</span> @endif
|
||||||
</p>
|
</p>
|
||||||
|
<p class="muted">Automatique toutes les 15 minutes (incrémentale) et chaque nuit (complète).</p>
|
||||||
|
</div>
|
||||||
<span class="spacer"></span>
|
<span class="spacer"></span>
|
||||||
@if (auth()->user()->hasRole('admin'))
|
@if ($isAdmin)
|
||||||
<form method="post" action="{{ route('admin.sync') }}" class="row" id="sync-form">
|
<form method="post" action="{{ route('admin.sync') }}" class="row" data-busy="Synchronisation en cours…">
|
||||||
@csrf
|
@csrf
|
||||||
<label class="muted" style="margin:0;font-weight:400">
|
<label class="switch">
|
||||||
<input type="checkbox" name="full" value="1"> complète
|
<input type="checkbox" name="full" value="1"><span class="switch-track"></span>Synchronisation complète
|
||||||
</label>
|
</label>
|
||||||
<button class="btn" type="submit" id="sync-btn">Synchroniser maintenant</button>
|
<button class="btn" type="submit"><x-icon name="refresh-cw" /><span data-label>Synchroniser maintenant</span></button>
|
||||||
</form>
|
</form>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
<div class="grid">
|
|
||||||
<div><div class="stat">{{ $users }}</div><div class="muted">agents synchronisés</div></div>
|
<div class="tiles">
|
||||||
<div><div class="stat">{{ $groups->count() }}</div><div class="muted">groupes signature_*</div></div>
|
<a class="tile" href="{{ route('admin.agents.index') }}">
|
||||||
<div><div class="stat"><a href="{{ route('admin.templates.index') }}">{{ $templates }}</a></div><div class="muted">templates</div></div>
|
<span class="tile-icon"><x-icon name="contact" :size="20" /></span>
|
||||||
<div><div class="stat">{{ $assignments }}</div><div class="muted">affectations</div></div>
|
<span><span class="tile-value">{{ $users }}</span><span class="tile-label">agents synchronisés</span></span>
|
||||||
<div><div class="stat"><a href="{{ route('admin.campaigns.index') }}">{{ $campaigns }}</a></div><div class="muted">campagnes en cours</div></div>
|
</a>
|
||||||
|
<a class="tile" href="{{ route('admin.templates.index') }}">
|
||||||
|
<span class="tile-icon"><x-icon name="file-code" :size="20" /></span>
|
||||||
|
<span><span class="tile-value">{{ $templates }}</span><span class="tile-label">templates</span></span>
|
||||||
|
</a>
|
||||||
|
@if ($isAdmin)
|
||||||
|
<a class="tile" href="{{ route('admin.assignments.index') }}">
|
||||||
|
<span class="tile-icon"><x-icon name="users" :size="20" /></span>
|
||||||
|
<span><span class="tile-value">{{ $assignments }}</span><span class="tile-label">affectations</span></span>
|
||||||
|
</a>
|
||||||
|
@else
|
||||||
|
<div class="tile">
|
||||||
|
<span class="tile-icon"><x-icon name="users" :size="20" /></span>
|
||||||
|
<span><span class="tile-value">{{ $assignments }}</span><span class="tile-label">affectations</span></span>
|
||||||
</div>
|
</div>
|
||||||
|
@endif
|
||||||
|
<a class="tile" href="{{ route('admin.campaigns.index') }}">
|
||||||
|
<span class="tile-icon"><x-icon name="megaphone" :size="20" /></span>
|
||||||
|
<span><span class="tile-value">{{ $campaigns }}</span><span class="tile-label">campagnes en cours</span></span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
|
||||||
<h2>Groupes synchronisés</h2>
|
<div class="card card--flush">
|
||||||
|
<div class="card-head">
|
||||||
|
<h2>Groupes AD synchronisés</h2>
|
||||||
|
<span class="badge plain">{{ $groups->count() }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="table-wrap">
|
||||||
<table class="list">
|
<table class="list">
|
||||||
<thead><tr><th>Groupe</th><th>Membres (transitifs)</th></tr></thead>
|
<thead><tr><th>Groupe</th><th class="right">Membres (imbriqués inclus)</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@forelse ($groups as $g)
|
@forelse ($groups as $g)
|
||||||
<tr><td>{{ $g->display_name }}</td><td>{{ $g->members_count }}</td></tr>
|
<tr><td>{{ $g->display_name }}</td><td class="right num">{{ $g->members_count }}</td></tr>
|
||||||
@empty
|
@empty
|
||||||
<tr><td colspan="2" class="muted">Aucun groupe : lancez « php artisan signature:sync --full ».</td></tr>
|
<tr><td colspan="2">
|
||||||
|
<div class="empty">
|
||||||
|
<x-icon name="users" :size="32" />
|
||||||
|
<p><strong>Aucun groupe synchronisé</strong>Cochez « complète » puis lancez une synchronisation pour importer les groupes signature_* de l'annuaire.</p>
|
||||||
|
</div>
|
||||||
|
</td></tr>
|
||||||
@endforelse
|
@endforelse
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@push('scripts')
|
|
||||||
<script>
|
|
||||||
document.getElementById('sync-form')?.addEventListener('submit', () => {
|
|
||||||
const b = document.getElementById('sync-btn');
|
|
||||||
b.disabled = true;
|
|
||||||
b.textContent = 'Synchronisation en cours…';
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
@endpush
|
|
||||||
|
|||||||
@@ -1,39 +1,49 @@
|
|||||||
@extends('layouts.app')
|
@extends('layouts.app')
|
||||||
@section('title', 'Champs agents — Signatures CABM')
|
@section('title', 'Champs agents — Signatures')
|
||||||
@section('content')
|
@section('content')
|
||||||
|
<header class="page-head">
|
||||||
|
<div>
|
||||||
<h1>Champs modifiables par les agents</h1>
|
<h1>Champs modifiables par les agents</h1>
|
||||||
<p class="muted" style="margin-top:-8px">
|
<p class="lead">
|
||||||
Les agents modifient ces champs dans l'add-in (bouton « Ma signature ») ou sur le portail.
|
Les agents modifient ces champs dans l'add-in (bouton « Ma signature ») ou sur le portail.
|
||||||
La valeur saisie remplace celle de l'annuaire <strong>dans la signature uniquement</strong> : l'AD reste la référence.
|
La valeur saisie remplace celle de l'annuaire <strong>dans la signature uniquement</strong> : l'AD reste la référence.
|
||||||
Publication immédiate, chaque modification est tracée. Fermer un champ fait revenir les signatures concernées à la valeur de l'annuaire.
|
Publication immédiate, chaque modification est tracée. Fermer un champ fait revenir les signatures concernées à la valeur de l'annuaire.
|
||||||
</p>
|
</p>
|
||||||
<form method="post" action="{{ route('admin.fields.update') }}" class="card">
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<form method="post" action="{{ route('admin.fields.update') }}" class="card card--flush">
|
||||||
@csrf @method('PUT')
|
@csrf @method('PUT')
|
||||||
|
<div class="table-wrap">
|
||||||
<table class="list">
|
<table class="list">
|
||||||
<thead><tr><th>Ouvert</th><th>Variable</th><th>Libellé affiché à l'agent</th><th>Contrôle</th><th>Agents ayant une valeur perso</th></tr></thead>
|
<thead><tr><th>Ouvert aux agents</th><th>Variable</th><th>Libellé affiché à l'agent</th><th>Contrôle de saisie</th><th class="right">Valeurs personnalisées</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@foreach ($fields as $f)
|
@foreach ($fields as $f)
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<input type="hidden" name="fields[{{ $f->field }}][enabled]" value="0">
|
<input type="hidden" name="fields[{{ $f->field }}][enabled]" value="0">
|
||||||
|
<label class="switch">
|
||||||
<input type="checkbox" name="fields[{{ $f->field }}][enabled]" value="1" @checked($f->enabled) aria-label="Ouvrir {{ $f->label }}">
|
<input type="checkbox" name="fields[{{ $f->field }}][enabled]" value="1" @checked($f->enabled) aria-label="Ouvrir {{ $f->label }}">
|
||||||
|
<span class="switch-track"></span>
|
||||||
|
</label>
|
||||||
</td>
|
</td>
|
||||||
<td><code>{{ $f->field }}</code></td>
|
<td><code>{{ $f->field }}</code></td>
|
||||||
<td><input type="text" name="fields[{{ $f->field }}][label]" value="{{ $f->label }}" maxlength="60" required></td>
|
<td><input type="text" name="fields[{{ $f->field }}][label]" value="{{ $f->label }}" maxlength="60" required aria-label="Libellé de {{ $f->field }}"></td>
|
||||||
<td>
|
<td>
|
||||||
<select name="fields[{{ $f->field }}][type]">
|
<select name="fields[{{ $f->field }}][type]" aria-label="Contrôle de {{ $f->field }}">
|
||||||
@foreach ($types as $key => $type)
|
@foreach ($types as $key => $type)
|
||||||
<option value="{{ $key }}" @selected(($f->validation_rule ?: 'text') === $key)>{{ $type['label'] }}</option>
|
<option value="{{ $key }}" @selected(($f->validation_rule ?: 'text') === $key)>{{ $type['label'] }}</option>
|
||||||
@endforeach
|
@endforeach
|
||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
<td class="muted">{{ $usage[$f->field] ?? 0 }}</td>
|
<td class="right num muted">{{ $usage[$f->field] ?? 0 }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@endforeach
|
@endforeach
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div class="row" style="margin-top:12px">
|
</div>
|
||||||
<button class="btn" type="submit">Enregistrer</button>
|
<div class="card-foot row">
|
||||||
|
<button class="btn" type="submit"><x-icon name="save" />Enregistrer</button>
|
||||||
<span class="muted">L'adresse e-mail n'est jamais modifiable.</span>
|
<span class="muted">L'adresse e-mail n'est jamais modifiable.</span>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,79 +1,90 @@
|
|||||||
@extends('layouts.app')
|
@extends('layouts.app')
|
||||||
@section('title', 'Images — Signatures CABM')
|
@section('title', 'Images — Signatures')
|
||||||
@section('content')
|
@section('content')
|
||||||
@php
|
@php
|
||||||
$tag = fn (string $inner) => '{'.'{'.$inner.'}'.'}';
|
$tag = fn (string $inner) => '{'.'{'.$inner.'}'.'}';
|
||||||
$isAdmin = auth()->user()->hasRole('admin');
|
$isAdmin = auth()->user()->hasRole('admin');
|
||||||
@endphp
|
@endphp
|
||||||
|
<header class="page-head">
|
||||||
|
<div>
|
||||||
<h1>Images</h1>
|
<h1>Images</h1>
|
||||||
<p class="muted" style="margin-top:-8px">
|
<p class="lead">
|
||||||
Logos et bannières utilisés dans les signatures, publiés sur <code>{{ config('signature.public_url') }}/img/</code>.
|
Logos et bannières des signatures, publiés sur <code>{{ config('signature.public_url') }}/img/</code>.
|
||||||
Les destinataires les chargent depuis ce serveur : visez moins de 100 Ko et la largeur réelle d'affichage (ex. 120 px pour un logo, 600 px pour une bannière).
|
Les destinataires les téléchargent depuis ce serveur : visez moins de 100 Ko et la largeur réelle d'affichage
|
||||||
|
(120 px pour un logo, 600 px pour une bannière).
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
<div class="card">
|
<form method="post" action="{{ route('admin.images.store') }}" enctype="multipart/form-data" class="card" data-busy="Envoi…">
|
||||||
<h2>Ajouter une image</h2>
|
|
||||||
<form method="post" action="{{ route('admin.images.store') }}" enctype="multipart/form-data" class="row" style="align-items:flex-end">
|
|
||||||
@csrf
|
@csrf
|
||||||
<div style="flex:2;min-width:240px">
|
<h2>Ajouter une image</h2>
|
||||||
<label for="image">Fichier (PNG, JPEG, GIF — {{ (int) ($maxKb / 1024) }} Mo max.)</label>
|
<div class="field-row" style="align-items:flex-end">
|
||||||
<input type="file" id="image" name="image" accept="image/png,image/jpeg,image/gif" required>
|
<div style="flex:2">
|
||||||
|
<label class="dropzone" data-dropzone>
|
||||||
|
<input type="file" id="image" name="image" accept="image/png,image/jpeg,image/gif" required aria-label="Fichier image">
|
||||||
|
<x-icon name="cloud-upload" :size="26" />
|
||||||
|
<span data-prompt>Glissez une image ici ou <u>parcourez vos fichiers</u></span>
|
||||||
|
<span class="dropzone-name" data-name hidden></span>
|
||||||
|
<span class="muted">PNG, JPEG ou GIF, {{ (int) ($maxKb / 1024) }} Mo maximum</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div style="flex:1;min-width:200px">
|
<div>
|
||||||
<label for="name">Nom (facultatif)</label>
|
<label for="name">Nom du fichier (facultatif)</label>
|
||||||
<input type="text" id="name" name="name" maxlength="80" placeholder="ex. logo-cabm">
|
<input type="text" id="name" name="name" maxlength="80" placeholder="ex. logo-cabm">
|
||||||
|
<p class="hint">Sans nom, celui du fichier envoyé est repris.</p>
|
||||||
|
<button class="btn" type="submit" style="margin-top:12px"><x-icon name="cloud-upload" /><span data-label>Envoyer</span></button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn" type="submit">Envoyer</button>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<div class="card-head">
|
||||||
|
<h2>Images en ligne</h2>
|
||||||
|
<span class="badge plain">{{ $images->count() }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
@if ($images->isEmpty())
|
||||||
<h2>Images en ligne ({{ $images->count() }})</h2>
|
<div class="card empty">
|
||||||
<table class="list">
|
<x-icon name="images" :size="32" />
|
||||||
<thead><tr><th style="width:150px">Aperçu</th><th>Fichier</th><th>À coller dans un template</th><th>Utilisée par</th><th></th></tr></thead>
|
<p><strong>Aucune image</strong>Commencez par le logo : le template fourni par défaut attend un fichier nommé logo-cabm.png.</p>
|
||||||
<tbody>
|
|
||||||
@forelse ($images as $img)
|
|
||||||
@php $snippet = '<img src="'.$tag('site_url').'/img/'.$img['name'].'" alt="" width="'.($img['width'] ?? '').'" style="display:block;border:0">'; @endphp
|
|
||||||
<tr>
|
|
||||||
<td><img src="{{ asset('img/'.$img['name']) }}" alt="" style="max-width:140px;max-height:70px;border:1px solid var(--line);background:#fff"></td>
|
|
||||||
<td>
|
|
||||||
<a href="{{ $img['url'] }}" target="_blank" rel="noopener">{{ $img['name'] }}</a>
|
|
||||||
<div class="muted">{{ $img['width'] }}×{{ $img['height'] }} px · {{ $img['kb'] }} Ko
|
|
||||||
@if ($img['kb'] > 150) <span class="badge warn">lourde</span> @endif</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div class="row">
|
|
||||||
<code style="font-size:12px;word-break:break-all">{{ $snippet }}</code>
|
|
||||||
<button type="button" class="btn secondary small" data-copy="{{ $snippet }}">Copier</button>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
@else
|
||||||
<td class="muted">{{ $img['used_in'] ? implode(', ', $img['used_in']) : '—' }}</td>
|
<div class="gallery">
|
||||||
<td style="text-align:right">
|
@foreach ($images as $img)
|
||||||
|
@php $snippet = '<img src="'.$tag('site_url').'/img/'.$img['name'].'" alt="" width="'.($img['width'] ?? '').'" style="display:block;border:0">'; @endphp
|
||||||
|
<article class="media">
|
||||||
|
<a class="media-thumb" href="{{ $img['url'] }}" target="_blank" rel="noopener" title="Ouvrir l'image publique">
|
||||||
|
<img src="{{ asset('img/'.$img['name']) }}" alt="" loading="lazy">
|
||||||
|
</a>
|
||||||
|
<div class="media-body">
|
||||||
|
<div class="media-name">{{ $img['name'] }}</div>
|
||||||
|
<div class="muted">
|
||||||
|
{{ $img['width'] }}×{{ $img['height'] }} px, {{ $img['kb'] }} Ko
|
||||||
|
@if ($img['kb'] > 150) <span class="badge warn">lourde</span> @endif
|
||||||
|
</div>
|
||||||
|
<div class="muted" style="margin-top:6px">
|
||||||
|
{{ $img['used_in'] ? 'Utilisée par : '.implode(', ', $img['used_in']) : 'Non utilisée' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="media-foot">
|
||||||
|
<button type="button" class="btn secondary small" data-copy="{{ $snippet }}" title="{{ $snippet }}">
|
||||||
|
<x-icon name="copy" :size="15" /><span data-label>Copier la balise</span>
|
||||||
|
</button>
|
||||||
|
<span class="spacer"></span>
|
||||||
@if ($isAdmin)
|
@if ($isAdmin)
|
||||||
<form class="inline" method="post" action="{{ route('admin.images.destroy', $img['name']) }}"
|
<form class="inline" method="post" action="{{ route('admin.images.destroy', $img['name']) }}"
|
||||||
onsubmit="return confirm('Supprimer {{ $img['name'] }} ? Les signatures déjà envoyées qui l\'affichent ne la montreront plus.')">
|
data-confirm="Supprimer {{ $img['name'] }} ? Les messages déjà envoyés qui l'affichent ne la montreront plus."
|
||||||
|
data-confirm-label="Supprimer" data-confirm-danger>
|
||||||
@csrf @method('DELETE')
|
@csrf @method('DELETE')
|
||||||
<button class="btn danger small" type="submit" @disabled($img['used_in'])>Supprimer</button>
|
<button class="btn danger small icon-only" type="submit" @disabled($img['used_in'])
|
||||||
|
title="{{ $img['used_in'] ? 'Retirez-la d\'abord des templates et campagnes' : 'Supprimer' }}" aria-label="Supprimer {{ $img['name'] }}">
|
||||||
|
<x-icon name="trash-2" :size="15" />
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@endif
|
@endif
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
@empty
|
|
||||||
<tr><td colspan="5" class="muted">Aucune image. Commencez par le logo (ex. logo-cabm.png, utilisé par le template par défaut).</td></tr>
|
|
||||||
@endforelse
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
|
</article>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@push('scripts')
|
|
||||||
<script>
|
|
||||||
document.addEventListener('click', async (e) => {
|
|
||||||
const text = e.target.dataset && e.target.dataset.copy;
|
|
||||||
if (!text) return;
|
|
||||||
try { await navigator.clipboard.writeText(text); e.target.textContent = 'Copié'; }
|
|
||||||
catch { e.target.textContent = 'Copie impossible'; }
|
|
||||||
setTimeout(() => { e.target.textContent = 'Copier'; }, 1500);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
@endpush
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@extends('layouts.app')
|
@extends('layouts.app')
|
||||||
@section('title', ($template->exists ? $template->name : 'Nouveau template').' — Signatures CABM')
|
@section('title', ($template->exists ? $template->name : 'Nouveau template').' — Signatures')
|
||||||
@section('content')
|
@section('content')
|
||||||
@php
|
@php
|
||||||
$isAdmin = auth()->user()->hasRole('admin');
|
$isAdmin = auth()->user()->hasRole('admin');
|
||||||
@@ -15,28 +15,33 @@
|
|||||||
];
|
];
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<div class="row" style="margin-bottom:16px">
|
<header class="page-head">
|
||||||
<a href="{{ route('admin.templates.index') }}">← Templates</a>
|
<div>
|
||||||
<h1 style="margin:0 0 0 12px">{{ $template->exists ? $template->name : 'Nouveau template' }}</h1>
|
<a class="back" href="{{ route('admin.templates.index') }}"><x-icon name="chevron-left" :size="16" />Templates</a>
|
||||||
@if ($template->is_default) <span class="badge">par défaut</span> @endif
|
<div class="page-title">
|
||||||
|
<h1>{{ $template->exists ? $template->name : 'Nouveau template' }}</h1>
|
||||||
|
@if ($template->is_default) <span class="badge plain">par défaut</span> @endif
|
||||||
@if ($template->exists)
|
@if ($template->exists)
|
||||||
@if (! $template->published_version)
|
@if (! $template->published_version)
|
||||||
<span class="badge warn">jamais publié</span>
|
<span class="badge warn">jamais publié</span>
|
||||||
@elseif ($dirty)
|
@elseif ($dirty)
|
||||||
<span class="badge warn">brouillon modifié depuis v{{ $template->published_version }}</span>
|
<span class="badge warn">brouillon modifié depuis v{{ $template->published_version }}</span>
|
||||||
@else
|
@else
|
||||||
<span class="badge ok">publié · v{{ $template->published_version }}</span>
|
<span class="badge ok">publié, v{{ $template->published_version }}</span>
|
||||||
@endif
|
@endif
|
||||||
@endif
|
@endif
|
||||||
<span class="spacer"></span>
|
|
||||||
@if ($template->exists && $isAdmin)
|
|
||||||
<form class="inline" method="post" action="{{ route('admin.templates.publish', $template) }}"
|
|
||||||
onsubmit="return confirm('Publier le brouillon enregistré ? Il sera servi immédiatement aux agents concernés.')">
|
|
||||||
@csrf
|
|
||||||
<button class="btn" type="submit" @disabled(! $dirty && $template->published_version)>Publier</button>
|
|
||||||
</form>
|
|
||||||
@endif
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
@if ($template->exists && $isAdmin)
|
||||||
|
<div class="page-actions">
|
||||||
|
<form class="inline" method="post" action="{{ route('admin.templates.publish', $template) }}"
|
||||||
|
data-confirm="Publier le brouillon enregistré ? Il sera servi immédiatement aux agents concernés." data-confirm-label="Publier">
|
||||||
|
@csrf
|
||||||
|
<button class="btn" type="submit" @disabled(! $dirty && $template->published_version)><x-icon name="send" />Publier</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</header>
|
||||||
|
|
||||||
<form method="post" id="template-form"
|
<form method="post" id="template-form"
|
||||||
action="{{ $template->exists ? route('admin.templates.update', $template) : route('admin.templates.store') }}">
|
action="{{ $template->exists ? route('admin.templates.update', $template) : route('admin.templates.store') }}">
|
||||||
@@ -57,16 +62,16 @@
|
|||||||
<label for="draft_html_short">Signature courte (réponses et transferts)</label>
|
<label for="draft_html_short">Signature courte (réponses et transferts)</label>
|
||||||
<textarea class="code" id="draft_html_short" name="draft_html_short" data-variant="short" spellcheck="false" style="min-height:120px">{{ old('draft_html_short', $template->draft_html_short) }}</textarea>
|
<textarea class="code" id="draft_html_short" name="draft_html_short" data-variant="short" spellcheck="false" style="min-height:120px">{{ old('draft_html_short', $template->draft_html_short) }}</textarea>
|
||||||
|
|
||||||
<div class="row" style="margin-top:12px">
|
<div class="row" style="margin-top:14px">
|
||||||
<button class="btn" type="submit">Enregistrer le brouillon</button>
|
<button class="btn" type="submit"><x-icon name="save" />Enregistrer le brouillon</button>
|
||||||
<span class="muted">L'enregistrement ne change rien pour les agents tant que le template n'est pas publié.</span>
|
<span class="muted">Rien ne change pour les agents tant que le template n'est pas publié.</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="row">
|
<div class="card-head">
|
||||||
<h2 style="margin:0">Aperçu</h2>
|
<h2>Aperçu</h2>
|
||||||
<span class="spacer"></span>
|
<span class="spacer"></span>
|
||||||
<select id="preview-variant" aria-label="Variante affichée" style="width:auto">
|
<select id="preview-variant" aria-label="Variante affichée" style="width:auto">
|
||||||
<option value="full">Complète</option>
|
<option value="full">Complète</option>
|
||||||
@@ -95,7 +100,7 @@
|
|||||||
<button type="button" data-insert="{{ $snippets['site_url'] }}" title="URL publique (images)">site_url</button>
|
<button type="button" data-insert="{{ $snippets['site_url'] }}" title="URL publique (images)">site_url</button>
|
||||||
<button type="button" data-insert="{{ $snippets['if'] }}" title="Ligne conditionnelle">#if …</button>
|
<button type="button" data-insert="{{ $snippets['if'] }}" title="Ligne conditionnelle">#if …</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="muted">Images : déposez-les dans <code>public/img</code> et référencez
|
<p class="muted">Images : envoyez-les depuis l'écran <a href="{{ route('admin.images.index') }}">Images</a> et référencez
|
||||||
<code>{{ $snippets['img'] }}</code>. Pas de script ni de formulaire : ils sont retirés à l'enregistrement.</p>
|
<code>{{ $snippets['img'] }}</code>. Pas de script ni de formulaire : ils sont retirés à l'enregistrement.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -103,21 +108,22 @@
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
@if ($template->exists)
|
@if ($template->exists)
|
||||||
<div class="card">
|
<div class="card card--flush">
|
||||||
<h2>Historique des publications</h2>
|
<div class="card-head"><h2>Historique des publications</h2></div>
|
||||||
|
<div class="table-wrap">
|
||||||
<table class="list">
|
<table class="list">
|
||||||
<thead><tr><th>Version</th><th>Publiée le</th><th>Par</th><th></th></tr></thead>
|
<thead><tr><th>Version</th><th>Publiée le</th><th>Par</th><th></th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@forelse ($versions as $v)
|
@forelse ($versions as $v)
|
||||||
<tr>
|
<tr>
|
||||||
<td>v{{ $v->version }} @if ($v->version === $template->published_version) <span class="badge ok">en ligne</span> @endif</td>
|
<td class="num">v{{ $v->version }} @if ($v->version === $template->published_version) <span class="badge ok">en ligne</span> @endif</td>
|
||||||
<td>{{ $v->created_at->timezone('Europe/Paris')->format('d/m/Y H:i') }}</td>
|
<td class="nowrap">{{ $v->created_at->timezone('Europe/Paris')->format('d/m/Y H:i') }}</td>
|
||||||
<td>{{ $v->publisher?->name ?? '—' }}</td>
|
<td>{{ $v->publisher?->name ?? '—' }}</td>
|
||||||
<td style="text-align:right">
|
<td class="cell-actions">
|
||||||
<form class="inline" method="post" action="{{ route('admin.templates.restore', [$template, $v]) }}"
|
<form class="inline" method="post" action="{{ route('admin.templates.restore', [$template, $v]) }}"
|
||||||
onsubmit="return confirm('Remplacer le brouillon actuel par la version {{ $v->version }} ?')">
|
data-confirm="Remplacer le brouillon actuel par la version {{ $v->version }} ?" data-confirm-label="Recharger">
|
||||||
@csrf
|
@csrf
|
||||||
<button class="btn secondary small" type="submit">Recharger dans le brouillon</button>
|
<button class="btn secondary small" type="submit"><x-icon name="rotate-ccw" :size="15" />Recharger dans le brouillon</button>
|
||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -127,19 +133,20 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
@if ($isAdmin)
|
@if ($isAdmin)
|
||||||
<div class="card row">
|
<div class="card row">
|
||||||
@unless ($template->is_default)
|
@unless ($template->is_default)
|
||||||
<form class="inline" method="post" action="{{ route('admin.templates.default', $template) }}">
|
<form class="inline" method="post" action="{{ route('admin.templates.default', $template) }}">
|
||||||
@csrf
|
@csrf
|
||||||
<button class="btn secondary" type="submit" @disabled(! $template->html_full)>Définir comme template par défaut</button>
|
<button class="btn secondary" type="submit" @disabled(! $template->html_full)><x-icon name="star" />Définir comme template par défaut</button>
|
||||||
</form>
|
</form>
|
||||||
<span class="spacer"></span>
|
<span class="spacer"></span>
|
||||||
<form class="inline" method="post" action="{{ route('admin.templates.destroy', $template) }}"
|
<form class="inline" method="post" action="{{ route('admin.templates.destroy', $template) }}"
|
||||||
onsubmit="return confirm('Supprimer ce template et ses affectations ?')">
|
data-confirm="Supprimer ce template et ses affectations ?" data-confirm-label="Supprimer" data-confirm-danger>
|
||||||
@csrf @method('DELETE')
|
@csrf @method('DELETE')
|
||||||
<button class="btn danger" type="submit">Supprimer</button>
|
<button class="btn danger" type="submit"><x-icon name="trash-2" />Supprimer</button>
|
||||||
</form>
|
</form>
|
||||||
@else
|
@else
|
||||||
<span class="muted">Le template par défaut s'applique aux agents qui ne sont dans aucun groupe affecté. Il ne peut pas être supprimé.</span>
|
<span class="muted">Le template par défaut s'applique aux agents qui ne sont dans aucun groupe affecté. Il ne peut pas être supprimé.</span>
|
||||||
|
|||||||
@@ -1,20 +1,26 @@
|
|||||||
@extends('layouts.app')
|
@extends('layouts.app')
|
||||||
@section('title', 'Templates — Signatures CABM')
|
@section('title', 'Templates — Signatures')
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="row" style="margin-bottom:16px">
|
<header class="page-head">
|
||||||
<h1 style="margin:0">Templates</h1>
|
<div>
|
||||||
<span class="spacer"></span>
|
<h1>Templates</h1>
|
||||||
<a class="btn" href="{{ route('admin.templates.create') }}">Nouveau template</a>
|
<p class="lead">Un template est le modèle HTML d'une signature. Il est servi aux agents une fois publié, selon les affectations par groupe AD.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="page-actions">
|
||||||
|
<a class="btn" href="{{ route('admin.templates.create') }}"><x-icon name="plus" />Nouveau template</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="card card--flush">
|
||||||
|
<div class="table-wrap">
|
||||||
<table class="list">
|
<table class="list">
|
||||||
<thead><tr><th>Nom</th><th>État</th><th>Version publiée</th><th>Groupes affectés</th><th>Mis à jour</th></tr></thead>
|
<thead><tr><th>Nom</th><th>État</th><th>Version publiée</th><th>Groupes affectés</th><th>Mis à jour</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@forelse ($templates as $t)
|
@forelse ($templates as $t)
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ route('admin.templates.edit', $t) }}">{{ $t->name }}</a>
|
<a class="cell-title" href="{{ route('admin.templates.edit', $t) }}">{{ $t->name }}</a>
|
||||||
@if ($t->is_default) <span class="badge">par défaut</span> @endif
|
@if ($t->is_default) <span class="badge plain">par défaut</span> @endif
|
||||||
@if ($t->description)<div class="muted">{{ $t->description }}</div>@endif
|
@if ($t->description)<div class="muted">{{ $t->description }}</div>@endif
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -26,14 +32,21 @@
|
|||||||
<span class="badge ok">publié</span>
|
<span class="badge ok">publié</span>
|
||||||
@endif
|
@endif
|
||||||
</td>
|
</td>
|
||||||
<td>{{ $t->published_version ? 'v'.$t->published_version : '—' }}</td>
|
<td class="num">{{ $t->published_version ? 'v'.$t->published_version : '—' }}</td>
|
||||||
<td>{{ $t->assignments_count }}</td>
|
<td class="num">{{ $t->assignments_count }}</td>
|
||||||
<td class="muted">{{ $t->updated_at?->timezone('Europe/Paris')->format('d/m/Y H:i') }}</td>
|
<td class="muted nowrap">{{ $t->updated_at?->timezone('Europe/Paris')->format('d/m/Y H:i') }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@empty
|
@empty
|
||||||
<tr><td colspan="5" class="muted">Aucun template.</td></tr>
|
<tr><td colspan="5">
|
||||||
|
<div class="empty">
|
||||||
|
<x-icon name="file-code" :size="32" />
|
||||||
|
<p><strong>Aucun template</strong>Créez le premier modèle de signature, puis publiez-le pour qu'il soit servi aux agents.</p>
|
||||||
|
<p><a class="btn" href="{{ route('admin.templates.create') }}"><x-icon name="plus" />Nouveau template</a></p>
|
||||||
|
</div>
|
||||||
|
</td></tr>
|
||||||
@endforelse
|
@endforelse
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
{{-- Icônes Lucide (licence ISC), intégrées pour ne dépendre d'aucun CDN. Usage : <x-icon name="plus" /> --}}
|
||||||
|
@props(['name', 'size' => 18])
|
||||||
|
@php
|
||||||
|
$icons = [
|
||||||
|
'layout-dashboard' => '<rect width="7" height="9" x="3" y="3" rx="1"/> <rect width="7" height="5" x="14" y="3" rx="1"/> <rect width="7" height="9" x="14" y="12" rx="1"/> <rect width="7" height="5" x="3" y="16" rx="1"/>',
|
||||||
|
'file-code' => '<path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/> <path d="M14 2v5a1 1 0 0 0 1 1h5"/> <path d="M10 12.5 8 15l2 2.5"/> <path d="m14 12.5 2 2.5-2 2.5"/>',
|
||||||
|
'images' => '<path d="m22 11-1.296-1.296a2.4 2.4 0 0 0-3.408 0L11 16"/> <path d="M4 8a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2"/> <circle cx="13" cy="7" r="1" fill="currentColor"/> <rect x="8" y="2" width="14" height="14" rx="2"/>',
|
||||||
|
'megaphone' => '<path d="M11 6a13 13 0 0 0 8.4-2.8A1 1 0 0 1 21 4v12a1 1 0 0 1-1.6.8A13 13 0 0 0 11 14H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z"/> <path d="M6 14a12 12 0 0 0 2.4 7.2 2 2 0 0 0 3.2-2.4A8 8 0 0 1 10 14"/> <path d="M8 6v8"/>',
|
||||||
|
'users' => '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/> <path d="M16 3.128a4 4 0 0 1 0 7.744"/> <path d="M22 21v-2a4 4 0 0 0-3-3.87"/> <circle cx="9" cy="7" r="4"/>',
|
||||||
|
'sliders-horizontal' => '<path d="M10 5H3"/> <path d="M12 19H3"/> <path d="M14 3v4"/> <path d="M16 17v4"/> <path d="M21 12h-9"/> <path d="M21 19h-5"/> <path d="M21 5h-7"/> <path d="M8 10v4"/> <path d="M8 12H3"/>',
|
||||||
|
'contact' => '<path d="M16 2v2"/> <path d="M7 21v-2a2 2 0 012-2h6a2 2 0 012 2v2"/> <path d="M8 2v2"/> <circle cx="12" cy="10" r="3"/> <rect x="3" y="3" width="18" height="18" rx="2"/>',
|
||||||
|
'signature' => '<path d="m21 17-2.156-1.868A.5.5 0 0 0 18 15.5v.5a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1c0-2.545-3.991-3.97-8.5-4a1 1 0 0 0 0 5c4.153 0 4.745-11.295 5.708-13.5a2.5 2.5 0 1 1 3.31 3.284"/> <path d="M3 21h18"/>',
|
||||||
|
'log-out' => '<path d="m16 17 5-5-5-5"/> <path d="M21 12H9"/> <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>',
|
||||||
|
'plus' => '<path d="M5 12h14"/> <path d="M12 5v14"/>',
|
||||||
|
'pencil' => '<path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/> <path d="m15 5 4 4"/>',
|
||||||
|
'trash-2' => '<path d="M10 11v6"/> <path d="M14 11v6"/> <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/> <path d="M3 6h18"/> <path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>',
|
||||||
|
'copy' => '<rect width="14" height="14" x="8" y="8" rx="2" ry="2"/> <path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>',
|
||||||
|
'check' => '<path d="M20 6 9 17l-5-5"/>',
|
||||||
|
'refresh-cw' => '<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/> <path d="M21 3v5h-5"/> <path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/> <path d="M8 16H3v5"/>',
|
||||||
|
'cloud-upload' => '<path d="M12 13v8"/> <path d="M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242"/> <path d="m8 17 4-4 4 4"/>',
|
||||||
|
'search' => '<path d="m21 21-4.34-4.34"/> <circle cx="11" cy="11" r="8"/>',
|
||||||
|
'chevron-left' => '<path d="m15 18-6-6 6-6"/>',
|
||||||
|
'chevron-right' => '<path d="m9 18 6-6-6-6"/>',
|
||||||
|
'triangle-alert' => '<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/> <path d="M12 9v4"/> <path d="M12 17h.01"/>',
|
||||||
|
'info' => '<circle cx="12" cy="12" r="10"/> <path d="M12 16v-4"/> <path d="M12 8h.01"/>',
|
||||||
|
'circle-check' => '<circle cx="12" cy="12" r="10"/> <path d="m16 9-5.5 5.5L8 12"/>',
|
||||||
|
'x' => '<path d="M18 6 6 18"/> <path d="m6 6 12 12"/>',
|
||||||
|
'eye' => '<path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/> <circle cx="12" cy="12" r="3"/>',
|
||||||
|
'power' => '<path d="M12 2v10"/> <path d="M18.4 6.6a9 9 0 1 1-12.77.04"/>',
|
||||||
|
'calendar-clock' => '<path d="M16 14v2.2l1.6 1"/> <path d="M16 2v3"/> <path d="M21 7.338V5a2 2 0 00-2-2H5a2 2 0 00-2 2v14a2 2 0 002 2h2.338"/> <path d="M3 9h5.859"/> <path d="M8 2v3"/> <circle cx="16" cy="16" r="6"/>',
|
||||||
|
'link' => '<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/> <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>',
|
||||||
|
'external-link' => '<path d="M15 3h6v6"/> <path d="M10 14 21 3"/> <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>',
|
||||||
|
'history' => '<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/> <path d="M3 3v5h5"/> <path d="M12 7v5l4 2"/>',
|
||||||
|
'star' => '<path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"/>',
|
||||||
|
'rotate-ccw' => '<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/> <path d="M3 3v5h5"/>',
|
||||||
|
'send' => '<path d="M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z"/> <path d="m21.854 2.147-10.94 10.939"/>',
|
||||||
|
'save' => '<path d="M15.2 3a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z"/> <path d="M17 21v-7a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v7"/> <path d="M7 3v4a1 1 0 0 0 1 1h7"/>',
|
||||||
|
'building-2' => '<path d="M10 12h4"/> <path d="M10 8h4"/> <path d="M14 21v-3a2 2 0 0 0-4 0v3"/> <path d="M6 10H4a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-2"/> <path d="M6 21V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v16"/>',
|
||||||
|
'mail' => '<path d="m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7"/> <rect x="2" y="4" width="20" height="16" rx="2"/>',
|
||||||
|
];
|
||||||
|
@endphp
|
||||||
|
<svg {{ $attributes->merge(['class' => 'icon']) }} xmlns="http://www.w3.org/2000/svg" width="{{ $size }}" height="{{ $size }}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">{!! $icons[$name] ?? '' !!}</svg>
|
||||||
@@ -1,93 +1,129 @@
|
|||||||
|
@php
|
||||||
|
$user = auth()->user();
|
||||||
|
$hasNav = (bool) $user?->role;
|
||||||
|
$isAdmin = $hasNav && $user->hasRole('admin');
|
||||||
|
$org = $user?->tenant?->name ?? config('signature.default_tenant.name');
|
||||||
|
$roleLabel = ['superadmin' => 'Super-administrateur', 'admin' => 'Administrateur', 'editor' => 'Éditeur'][$user?->role] ?? 'Agent';
|
||||||
|
$initials = $user ? mb_strtoupper(collect(preg_split('/[\s\-]+/u', trim($user->name)))->filter()->take(2)->map(fn ($w) => mb_substr($w, 0, 1))->join('')) : '';
|
||||||
|
// Version des fichiers statiques = date de modification : le navigateur recharge après chaque mise à jour.
|
||||||
|
$asset = fn (string $path) => asset($path).'?v='.(@filemtime(public_path($path)) ?: '1');
|
||||||
|
$nav = fn (string $route, string $pattern, string $icon, string $label) => compact('route', 'pattern', 'icon', 'label');
|
||||||
|
$links = $hasNav ? array_filter([
|
||||||
|
['group' => null, 'items' => [$nav('admin.dashboard', 'admin.dashboard', 'layout-dashboard', 'Tableau de bord')]],
|
||||||
|
['group' => 'Contenu', 'items' => [
|
||||||
|
$nav('admin.templates.index', 'admin.templates.*', 'file-code', 'Templates'),
|
||||||
|
$nav('admin.images.index', 'admin.images.*', 'images', 'Images'),
|
||||||
|
$nav('admin.campaigns.index', 'admin.campaigns.*', 'megaphone', 'Campagnes'),
|
||||||
|
]],
|
||||||
|
['group' => 'Diffusion', 'items' => array_values(array_filter([
|
||||||
|
$isAdmin ? $nav('admin.assignments.index', 'admin.assignments.*', 'users', 'Affectations') : null,
|
||||||
|
$isAdmin ? $nav('admin.fields.index', 'admin.fields.*', 'sliders-horizontal', 'Champs agents') : null,
|
||||||
|
$nav('admin.agents.index', 'admin.agents.*', 'contact', 'Agents'),
|
||||||
|
]))],
|
||||||
|
]) : [];
|
||||||
|
@endphp
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="fr">
|
<html lang="fr">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
<title>@yield('title', 'Signatures CABM')</title>
|
<title>@yield('title', 'Signatures')</title>
|
||||||
<style>
|
<script>document.documentElement.classList.add('js');</script>
|
||||||
:root { --ink:#1b1b1b; --muted:#5c5c5c; --line:#dcdcdc; --accent:#0b5cad; --accent-soft:#e7f0fa; --bg:#f6f7f9; --ok:#1f7a3d; --warn:#9a5b00; --danger:#b3261e; }
|
<link rel="preload" href="{{ asset('fonts/atkinson-next.woff2') }}" as="font" type="font/woff2" crossorigin>
|
||||||
* { box-sizing: border-box; }
|
<link rel="stylesheet" href="{{ $asset('css/app.css') }}">
|
||||||
body { margin:0; font-family:"Segoe UI", system-ui, sans-serif; color:var(--ink); background:var(--bg); font-size:15px; }
|
|
||||||
header { background:#fff; border-bottom:1px solid var(--line); padding:0 24px; display:flex; justify-content:space-between; align-items:center; gap:16px; flex-wrap:wrap; }
|
|
||||||
header .brand { color:var(--accent); font-weight:600; padding:14px 0; }
|
|
||||||
nav a { display:inline-block; padding:14px 10px; color:var(--muted); text-decoration:none; border-bottom:2px solid transparent; }
|
|
||||||
nav a.active, nav a:hover { color:var(--ink); border-bottom-color:var(--accent); }
|
|
||||||
main { max-width:1200px; margin:24px auto; padding:0 16px; }
|
|
||||||
h1 { font-size:22px; margin:0 0 16px; }
|
|
||||||
h2 { font-size:17px; margin:0 0 12px; }
|
|
||||||
.card { background:#fff; border:1px solid var(--line); border-radius:8px; padding:20px; margin-bottom:16px; }
|
|
||||||
.grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(160px,1fr)); gap:12px; }
|
|
||||||
.split { display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr); gap:16px; align-items:start; }
|
|
||||||
@media (max-width: 900px) { .split { grid-template-columns:1fr; } }
|
|
||||||
.stat { font-size:28px; font-weight:600; }
|
|
||||||
.muted { color:var(--muted); font-size:13px; }
|
|
||||||
.row { display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
|
|
||||||
.spacer { flex:1; }
|
|
||||||
table.list { width:100%; border-collapse:collapse; }
|
|
||||||
table.list th, table.list td { text-align:left; padding:8px; border-bottom:1px solid var(--line); vertical-align:middle; }
|
|
||||||
table.list th { font-size:13px; color:var(--muted); font-weight:600; }
|
|
||||||
label { display:block; font-weight:600; font-size:13px; margin:12px 0 4px; }
|
|
||||||
input[type=text], input[type=number], input[type=search], input[type=url], input[type=datetime-local], select, textarea { width:100%; padding:8px 10px; border:1px solid var(--line); border-radius:6px; font:inherit; background:#fff; }
|
|
||||||
textarea.code { font-family:Consolas, "Cascadia Mono", monospace; font-size:13px; line-height:1.45; min-height:260px; resize:vertical; tab-size:2; }
|
|
||||||
input.small { width:90px; }
|
|
||||||
.btn { display:inline-block; background:var(--accent); color:#fff; border:1px solid var(--accent); border-radius:6px; padding:7px 14px; font:inherit; font-size:14px; cursor:pointer; text-decoration:none; }
|
|
||||||
.btn.secondary { background:#fff; color:var(--accent); }
|
|
||||||
.btn.danger { background:#fff; color:var(--danger); border-color:var(--danger); }
|
|
||||||
.btn.small { padding:4px 10px; font-size:13px; }
|
|
||||||
.btn:disabled { opacity:.5; cursor:default; }
|
|
||||||
button.link { background:none; border:0; color:var(--accent); cursor:pointer; font:inherit; padding:0; }
|
|
||||||
.badge { display:inline-block; padding:2px 8px; border-radius:10px; font-size:12px; background:var(--accent-soft); color:var(--accent); }
|
|
||||||
.badge.ok { background:#e5f4ea; color:var(--ok); }
|
|
||||||
.badge.warn { background:#fdf1dc; color:var(--warn); }
|
|
||||||
.flash { padding:10px 14px; border-radius:6px; margin-bottom:16px; background:#e5f4ea; color:var(--ok); border:1px solid #bfe3cb; }
|
|
||||||
.errors { padding:10px 14px; border-radius:6px; margin-bottom:16px; background:#fde8e6; color:var(--danger); border:1px solid #f5c2bd; }
|
|
||||||
.notice { padding:10px 14px; border-radius:6px; margin-bottom:16px; background:#fdf1dc; color:var(--warn); border:1px solid #f3d9a4; }
|
|
||||||
label.check { font-weight:400; font-size:14px; margin:4px 0; }
|
|
||||||
label.check input { margin-right:6px; }
|
|
||||||
fieldset { border:1px solid var(--line); border-radius:6px; margin:12px 0 0; padding:8px 12px 12px; }
|
|
||||||
legend { font-weight:600; font-size:13px; padding:0 4px; }
|
|
||||||
.scroll { max-height:220px; overflow:auto; }
|
|
||||||
.chips button { background:var(--accent-soft); color:var(--accent); border:0; border-radius:12px; padding:3px 9px; margin:2px; font:12px Consolas, monospace; cursor:pointer; }
|
|
||||||
iframe.sig { width:100%; min-height:220px; border:1px solid var(--line); border-radius:6px; background:#fff; resize:vertical; }
|
|
||||||
form.inline { display:inline; }
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<a class="skip" href="#main">Aller au contenu</a>
|
||||||
<span class="brand">Signatures CABM</span>
|
<div @class(['app', 'app--nav' => $hasNav])>
|
||||||
@auth
|
@auth
|
||||||
@if (auth()->user()->role)
|
@if ($hasNav)
|
||||||
<nav>
|
<aside class="sidebar">
|
||||||
<a href="{{ route('admin.dashboard') }}" @class(['active' => request()->routeIs('admin.dashboard')])>Tableau de bord</a>
|
<a class="brand" href="{{ route('admin.dashboard') }}">
|
||||||
<a href="{{ route('admin.templates.index') }}" @class(['active' => request()->routeIs('admin.templates.*')])>Templates</a>
|
<span class="brand-name">Signatures</span>
|
||||||
<a href="{{ route('admin.images.index') }}" @class(['active' => request()->routeIs('admin.images.*')])>Images</a>
|
<svg class="brand-stroke" viewBox="0 0 104 12" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true">
|
||||||
<a href="{{ route('admin.campaigns.index') }}" @class(['active' => request()->routeIs('admin.campaigns.*')])>Campagnes</a>
|
<path d="M2 8c9-6 15-6 20-1s9 4 14-1 10-5 15 0 9 4 14 0 11-5 17-1 12 4 20-1"/>
|
||||||
@if (auth()->user()->hasRole('admin'))
|
</svg>
|
||||||
<a href="{{ route('admin.assignments.index') }}" @class(['active' => request()->routeIs('admin.assignments.*')])>Affectations</a>
|
<span class="brand-org">{{ $org }}</span>
|
||||||
<a href="{{ route('admin.fields.index') }}" @class(['active' => request()->routeIs('admin.fields.*')])>Champs agents</a>
|
</a>
|
||||||
@endif
|
<nav class="nav" aria-label="Navigation principale">
|
||||||
<a href="{{ route('admin.agents.index') }}" @class(['active' => request()->routeIs('admin.agents.*')])>Agents</a>
|
@foreach ($links as $section)
|
||||||
<a href="{{ route('portal.home') }}" @class(['active' => request()->routeIs('portal.*')])>Ma signature</a>
|
@if ($section['group'])<p class="nav-group">{{ $section['group'] }}</p>@endif
|
||||||
|
@foreach ($section['items'] as $item)
|
||||||
|
<a href="{{ route($item['route']) }}" @class(['nav-link', 'active' => request()->routeIs($item['pattern'])])
|
||||||
|
@if (request()->routeIs($item['pattern'])) aria-current="page" @endif>
|
||||||
|
<x-icon :name="$item['icon']" />{{ $item['label'] }}
|
||||||
|
</a>
|
||||||
|
@endforeach
|
||||||
|
@endforeach
|
||||||
</nav>
|
</nav>
|
||||||
@endif
|
<div class="sidebar-foot">
|
||||||
<form method="post" action="{{ route('logout') }}" class="row">@csrf
|
<a href="{{ route('portal.home') }}" @class(['nav-link', 'active' => request()->routeIs('portal.*')])>
|
||||||
<span class="muted">{{ auth()->user()->name }}@if (auth()->user()->role) · {{ auth()->user()->role }}@endif</span>
|
<x-icon name="signature" />Ma signature
|
||||||
<button class="link" type="submit">Se déconnecter</button>
|
</a>
|
||||||
|
<div class="user">
|
||||||
|
<span class="avatar" aria-hidden="true">{{ $initials }}</span>
|
||||||
|
<span>
|
||||||
|
<span class="user-name">{{ $user->name }}</span>
|
||||||
|
<span class="user-role">{{ $roleLabel }}</span>
|
||||||
|
</span>
|
||||||
|
<form method="post" action="{{ route('logout') }}">@csrf
|
||||||
|
<button class="icon-btn" type="submit" title="Se déconnecter" aria-label="Se déconnecter"><x-icon name="log-out" /></button>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
@else
|
||||||
|
<header class="topbar">
|
||||||
|
<a class="brand" href="{{ route('portal.home') }}"><span class="brand-name">Signatures</span></a>
|
||||||
|
<span class="muted" style="color:inherit">{{ $org }}</span>
|
||||||
|
<div class="user">
|
||||||
|
<span class="avatar" aria-hidden="true">{{ $initials }}</span>
|
||||||
|
<span class="user-name">{{ $user->name }}</span>
|
||||||
|
<form method="post" action="{{ route('logout') }}">@csrf
|
||||||
|
<button class="icon-btn" type="submit" title="Se déconnecter" aria-label="Se déconnecter"><x-icon name="log-out" /></button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
@endif
|
||||||
@endauth
|
@endauth
|
||||||
</header>
|
|
||||||
<main>
|
<div class="content">
|
||||||
|
<main id="main">
|
||||||
@if (session('status'))
|
@if (session('status'))
|
||||||
<div class="flash" role="status">{{ session('status') }}</div>
|
<div class="toasts">
|
||||||
|
<div class="toast" role="status" data-toast>
|
||||||
|
<x-icon name="circle-check" />
|
||||||
|
<span>{{ session('status') }}</span>
|
||||||
|
<button class="icon-btn" type="button" data-dismiss aria-label="Fermer"><x-icon name="x" :size="16" /></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@endif
|
@endif
|
||||||
@if ($errors->any())
|
@if ($errors->any())
|
||||||
<div class="errors" role="alert">
|
<div class="alert errors" role="alert">
|
||||||
@foreach ($errors->all() as $error)<div>{{ $error }}</div>@endforeach
|
<x-icon name="triangle-alert" />
|
||||||
|
<div>@foreach ($errors->all() as $error)<div>{{ $error }}</div>@endforeach</div>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
@yield('content')
|
@yield('content')
|
||||||
</main>
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dialog class="dialog" id="confirm-dialog" aria-labelledby="confirm-title">
|
||||||
|
<form method="dialog">
|
||||||
|
<div class="dialog-body">
|
||||||
|
<h2 id="confirm-title">Confirmer l'action</h2>
|
||||||
|
<p data-message></p>
|
||||||
|
</div>
|
||||||
|
<div class="dialog-actions">
|
||||||
|
<button class="btn secondary" value="cancel" autofocus>Annuler</button>
|
||||||
|
<button class="btn" value="ok" data-ok>Confirmer</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
|
<script src="{{ $asset('js/app.js') }}" defer></script>
|
||||||
@stack('scripts')
|
@stack('scripts')
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
{{-- Rendu isolé d'une signature : iframe sandbox (aucun script, origine opaque). --}}
|
{{-- Rendu isolé d'une signature : iframe sandbox (aucun script, origine opaque). --}}
|
||||||
<iframe class="sig" sandbox title="{{ $title ?? 'Aperçu de la signature' }}" @isset($id) id="{{ $id }}" @endisset
|
<iframe class="sig {{ $class ?? '' }}" sandbox title="{{ $title ?? 'Aperçu de la signature' }}" @isset($id) id="{{ $id }}" @endisset
|
||||||
srcdoc="{{ '<!doctype html><meta charset="utf-8"><base target="_blank"><body style="margin:12px">'.($html ?? '').'</body>' }}"></iframe>
|
srcdoc="{{ '<!doctype html><meta charset="utf-8"><base target="_blank"><body style="margin:12px">'.($html ?? '').'</body>' }}"></iframe>
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
@extends('layouts.app')
|
@extends('layouts.app')
|
||||||
@section('title', 'Ma signature — CABM')
|
@section('title', 'Ma signature — Signatures')
|
||||||
@section('content')
|
@section('content')
|
||||||
<h1>Ma signature</h1>
|
<header class="page-head">
|
||||||
<div class="split">
|
|
||||||
<div>
|
<div>
|
||||||
|
<h1>Ma signature</h1>
|
||||||
|
<p class="lead">Elle est insérée automatiquement dans Outlook. Modèle appliqué : {{ $full['template'] ?? 'aucun' }}.</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="split">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Nouveau message</h2>
|
<h2>Nouveau message</h2>
|
||||||
<p class="muted">Modèle : {{ $full['template'] ?? 'aucun' }}</p>
|
|
||||||
@include('partials.signature-frame', ['html' => $full['html'] ?? '<em>Aucune signature</em>'])
|
@include('partials.signature-frame', ['html' => $full['html'] ?? '<em>Aucune signature</em>'])
|
||||||
<h2 style="margin-top:16px">Réponse / transfert</h2>
|
<h2 style="margin-top:18px">Réponse et transfert</h2>
|
||||||
@include('partials.signature-frame', ['html' => ($reply['html'] ?? '') !== '' ? $reply['html'] : '<em>Aucune signature</em>'])
|
@include('partials.signature-frame', ['html' => ($reply['html'] ?? '') !== '' ? $reply['html'] : '<em>Aucune signature</em>', 'class' => 'short'])
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="post" action="{{ route('portal.update') }}" class="card">
|
<form method="post" action="{{ route('portal.update') }}" class="card">
|
||||||
@@ -24,29 +27,32 @@
|
|||||||
@foreach ($profile['fields'] as $f)
|
@foreach ($profile['fields'] as $f)
|
||||||
@php
|
@php
|
||||||
$mode = ! $f['overridden'] ? 'directory' : ($f['override'] === '' ? 'hidden' : 'custom');
|
$mode = ! $f['overridden'] ? 'directory' : ($f['override'] === '' ? 'hidden' : 'custom');
|
||||||
$id = 'f-'.$f['key'];
|
$fieldId = 'f-'.$f['key'];
|
||||||
@endphp
|
@endphp
|
||||||
<fieldset style="border:1px solid var(--line);border-radius:6px;padding:10px 12px;margin:12px 0" data-field>
|
<fieldset data-field>
|
||||||
<legend style="font-weight:600;font-size:13px;padding:0 4px">{{ $f['label'] }}</legend>
|
<legend>{{ $f['label'] }}</legend>
|
||||||
<label style="font-weight:400;margin:4px 0"><input type="radio" name="fields[{{ $f['key'] }}][mode]" value="directory" @checked($mode === 'directory')>
|
<label class="option"><input type="radio" name="fields[{{ $f['key'] }}][mode]" value="directory" @checked($mode === 'directory')>
|
||||||
Annuaire : <strong>{{ $f['directory_value'] ?: '(vide)' }}</strong></label>
|
<span>Annuaire : <strong>{{ $f['directory_value'] ?: '(vide)' }}</strong></span></label>
|
||||||
<label style="font-weight:400;margin:4px 0"><input type="radio" name="fields[{{ $f['key'] }}][mode]" value="custom" @checked($mode === 'custom')>
|
<label class="option"><input type="radio" name="fields[{{ $f['key'] }}][mode]" value="custom" @checked($mode === 'custom')>
|
||||||
Valeur personnalisée</label>
|
<span style="flex:1">Valeur personnalisée
|
||||||
<input type="text" id="{{ $id }}" name="fields[{{ $f['key'] }}][value]" value="{{ $mode === 'custom' ? $f['override'] : '' }}"
|
<input type="text" id="{{ $fieldId }}" name="fields[{{ $f['key'] }}][value]" value="{{ $mode === 'custom' ? $f['override'] : '' }}" style="margin-top:6px"
|
||||||
maxlength="120" @if ($f['type'] === 'phone') inputmode="tel" placeholder="ex. 06 12 34 56 78" @endif aria-label="{{ $f['label'] }} personnalisé">
|
maxlength="120" @if ($f['type'] === 'phone') inputmode="tel" placeholder="ex. 06 12 34 56 78" @endif aria-label="{{ $f['label'] }} personnalisé">
|
||||||
<label style="font-weight:400;margin:4px 0"><input type="radio" name="fields[{{ $f['key'] }}][mode]" value="hidden" @checked($mode === 'hidden')>
|
</span></label>
|
||||||
Ne pas afficher</label>
|
<label class="option"><input type="radio" name="fields[{{ $f['key'] }}][mode]" value="hidden" @checked($mode === 'hidden')>
|
||||||
|
<span>Ne pas afficher</span></label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@endforeach
|
@endforeach
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<h2 style="margin-top:16px">En réponse et transfert</h2>
|
|
||||||
@php $rv = $profile['reply_variant'] ?? 'default'; $labels = ['short' => 'signature courte', 'full' => 'signature complète', 'none' => 'aucune signature']; @endphp
|
@php $rv = $profile['reply_variant'] ?? 'default'; $labels = ['short' => 'signature courte', 'full' => 'signature complète', 'none' => 'aucune signature']; @endphp
|
||||||
|
<fieldset>
|
||||||
|
<legend>En réponse et transfert</legend>
|
||||||
@foreach (['default' => 'Réglage par défaut ('.$labels[$profile['reply_variant_default']].')', 'short' => 'Signature courte', 'full' => 'Signature complète', 'none' => 'Aucune signature'] as $value => $label)
|
@foreach (['default' => 'Réglage par défaut ('.$labels[$profile['reply_variant_default']].')', 'short' => 'Signature courte', 'full' => 'Signature complète', 'none' => 'Aucune signature'] as $value => $label)
|
||||||
<label style="font-weight:400;margin:4px 0"><input type="radio" name="reply_variant" value="{{ $value }}" @checked($rv === $value)> {{ $label }}</label>
|
<label class="option"><input type="radio" name="reply_variant" value="{{ $value }}" @checked($rv === $value)><span>{{ $label }}</span></label>
|
||||||
@endforeach
|
@endforeach
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
<div class="row" style="margin-top:16px"><button class="btn" type="submit">Enregistrer</button></div>
|
<div class="row" style="margin-top:16px"><button class="btn" type="submit"><x-icon name="save" />Enregistrer</button></div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
Reference in New Issue
Block a user