Refonte design V1

This commit is contained in:
mrKamoo
2026-10-02 13:06:14 +02:00
parent 429dd9de5e
commit 83e1ce00a9
20 changed files with 1403 additions and 531 deletions
@@ -1,40 +1,74 @@
@extends('layouts.app')
@section('title', 'Agents — Signatures CABM')
@section('title', 'Agents — Signatures')
@section('content')
<h1>Agents</h1>
@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>
<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">
<input type="search" name="q" value="{{ $q }}" placeholder="Nom, adresse ou service" style="max-width:360px" aria-label="Rechercher un agent">
<button class="btn" type="submit">Rechercher</button>
<span class="muted">{{ $agents->total() }} agent(s)</span>
<div class="search">
<x-icon name="search" :size="16" />
<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>
<div class="card">
<table class="list">
<thead><tr><th>Agent</th><th>Service</th><th>Groupes signature_*</th><th>Template appliqué</th></tr></thead>
<tbody>
@forelse ($agents as $a)
<tr>
<td><a href="{{ route('admin.agents.show', $a) }}">{{ $a->display_name }}</a><div class="muted">{{ $a->mail }}</div></td>
<td>{{ $a->department }}</td>
<td class="muted">{{ $a->groups->pluck('display_name')->join(', ') ?: '—' }}</td>
<td>{{ $resolved[$a->id] ?? '— aucun —' }}</td>
</tr>
@empty
<tr><td colspan="4" class="muted">Aucun agent trouvé.</td></tr>
@endforelse
</tbody>
</table>
<div class="card card--flush">
<div class="table-wrap">
<table class="list">
<thead><tr><th>Agent</th><th>Service</th><th>Groupes signature_*</th><th>Template appliqué</th></tr></thead>
<tbody>
@forelse ($agents as $a)
<tr>
<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 class="muted">{{ $a->groups->pluck('display_name')->join(', ') ?: '—' }}</td>
<td>
@if ($resolved[$a->id] ?? null)
{{ $resolved[$a->id] }}
@else
<span class="badge warn">aucun</span>
@endif
</td>
</tr>
@empty
<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
</tbody>
</table>
</div>
@if ($agents->hasPages())
<nav class="row" style="margin-top:12px" aria-label="Pagination">
<nav class="card-foot pager" aria-label="Pagination">
@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
<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
<span class="muted">Page {{ $agents->currentPage() }} / {{ $agents->lastPage() }}</span>
<span class="muted">Page {{ $agents->currentPage() }} sur {{ $agents->lastPage() }}</span>
@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
<span class="muted">Suivant →</span>
<span class="btn secondary small" aria-disabled="true">Suivant<x-icon name="chevron-right" :size="15" /></span>
@endif
</nav>
@endif
@@ -1,41 +1,51 @@
@extends('layouts.app')
@section('title', $agent->display_name.' — Signatures CABM')
@section('title', $agent->display_name.' — Signatures')
@section('content')
<div class="row" style="margin-bottom:16px">
<a href="{{ route('admin.agents.index') }}">← Agents</a>
<h1 style="margin:0 0 0 12px">{{ $agent->display_name }}</h1>
</div>
<header class="page-head">
<div>
<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>
</header>
<div class="split">
<div class="card">
<h2>Annuaire (Entra)</h2>
<table class="list">
<tr><th>Adresse</th><td>{{ $agent->mail }}</td></tr>
<tr><th>Fonction</th><td>{{ $agent->job_title }}</td></tr>
<tr><th>Service</th><td>{{ $agent->department }}</td></tr>
<tr><th>Téléphone</th><td>{{ $agent->business_phone }}</td></tr>
<tr><th>Mobile</th><td>{{ $agent->mobile_phone }}</td></tr>
<tr><th>Groupes signature_*</th><td>{{ $agent->groups->pluck('display_name')->join(', ') ?: '—' }}</td></tr>
<tr><th>Template appliqué</th><td>
<dl class="facts">
<dt>Adresse</dt><dd>{{ $agent->mail ?: '—' }}</dd>
<dt>Fonction</dt><dd>{{ $agent->job_title ?: '—' }}</dd>
<dt>Service</dt><dd>{{ $agent->department ?: '—' }}</dd>
<dt>Téléphone</dt><dd>{{ $agent->business_phone ?: '—' }}</dd>
<dt>Mobile</dt><dd>{{ $agent->mobile_phone ?: '—' }}</dd>
<dt>Groupes signature_*</dt><dd>{{ $agent->groups->pluck('display_name')->join(', ') ?: '—' }}</dd>
<dt>Template appliqué</dt>
<dd>
@if ($template)
<a href="{{ route('admin.templates.edit', $template) }}">{{ $template->name }}</a>
@else — aucun (pas de template par défaut publié) — @endif
</td></tr>
<tr><th>Bannière</th><td>
@else
<span class="badge warn">aucun</span> <span class="muted">pas de template par défaut publié</span>
@endif
</dd>
<dt>Bannière</dt>
<dd>
@if ($campaign)
<a href="{{ route('admin.campaigns.edit', $campaign) }}">{{ $campaign->name }}</a>
@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>
@endif
@else — aucune campagne en cours pour cet agent — @endif
</td></tr>
<tr><th>Synchronisé le</th><td class="muted">{{ $agent->synced_at?->timezone('Europe/Paris')->format('d/m/Y H:i') }}</td></tr>
</table>
@else
<span class="muted">Aucune campagne en cours pour cet agent</span>
@endif
</dd>
<dt>Synchronisé le</dt><dd class="muted">{{ $agent->synced_at?->timezone('Europe/Paris')->format('d/m/Y H:i') ?? '—' }}</dd>
</dl>
</div>
<div class="card">
<h2>Nouveau message</h2>
@include('partials.signature-frame', ['html' => $full['html'] ?? ''])
<h2 style="margin-top:16px">Réponse / transfert</h2>
@include('partials.signature-frame', ['html' => $reply['html'] ?? ''])
<h2 style="margin-top:18px">Réponse et transfert</h2>
@include('partials.signature-frame', ['html' => $reply['html'] ?? '', 'class' => 'short'])
</div>
</div>
@endsection
@@ -1,79 +1,92 @@
@extends('layouts.app')
@section('title', 'Affectations — Signatures CABM')
@section('title', 'Affectations — Signatures')
@section('content')
<h1>Affectations</h1>
<p class="muted" style="margin-top:-8px">
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.
Tous les groupes AD synchronisés sont proposés, sauf les groupes de rôles du back-office.
</p>
<header class="page-head">
<div>
<h1>Affectations</h1>
<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>
(à égalité : la plus ancienne). Sans groupe affecté, il reçoit le template par défaut.
</p>
</div>
</header>
<div class="card">
<form method="post" action="{{ route('admin.assignments.store') }}" class="card">
@csrf
<h2>Ajouter une affectation</h2>
<form method="post" action="{{ route('admin.assignments.store') }}" class="row" style="align-items:flex-end">
@csrf
<div style="flex:2;min-width:220px">
<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>
<option value="">— choisir —</option>
<option value="">Choisir un groupe</option>
@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
@foreach ($groups as $g)
<option value="{{ $g->id }}" @selected(old('directory_group_id') == $g->id)>{{ $g->display_name }} ({{ $g->members_count }})</option>
@endforeach
</select>
</div>
<div style="flex:2;min-width:220px">
<label for="template_id">Template</label>
<div style="flex:2">
<label for="template_id" style="margin-top:0">Template</label>
<select id="template_id" name="template_id" required>
<option value="">— choisir —</option>
<option value="">Choisir un template</option>
@foreach ($templates as $t)
<option value="{{ $t->id }}" @selected(old('template_id') == $t->id)>{{ $t->name }}{{ $t->html_full ? '' : ' (non publié)' }}</option>
@endforeach
</select>
</div>
<div>
<label for="priority">Priorité</label>
<input class="small" type="number" id="priority" name="priority" min="0" max="1000" value="{{ old('priority', 10) }}" required>
<div style="flex:0 0 96px;min-width:96px">
<label for="priority" style="margin-top:0">Priorité</label>
<input type="number" id="priority" name="priority" min="0" max="1000" value="{{ old('priority', 10) }}" required>
</div>
<button class="btn" type="submit">Ajouter</button>
</form>
</div>
<div style="flex:0 0 auto;min-width:0">
<button class="btn" type="submit"><x-icon name="plus" />Ajouter</button>
</div>
</div>
</form>
<div class="card">
<h2>Affectations actives, par ordre de priorité</h2>
<table class="list">
<thead><tr><th>Priorité</th><th>Groupe AD</th><th>Membres</th><th>Template</th><th></th></tr></thead>
<tbody>
@forelse ($assignments as $a)
<tr>
<td>
<form class="row" method="post" action="{{ route('admin.assignments.update', $a) }}">
@csrf @method('PUT')
<input class="small" type="number" name="priority" min="0" max="1000" value="{{ $a->priority }}" aria-label="Priorité">
<button class="btn secondary small" type="submit">OK</button>
</form>
</td>
<td>{{ $a->group?->display_name }}</td>
<td>{{ $a->group?->members_count }}</td>
<td>
<a 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
</td>
<td style="text-align:right">
<form class="inline" method="post" action="{{ route('admin.assignments.destroy', $a) }}"
onsubmit="return confirm('Supprimer cette affectation ?')">
@csrf @method('DELETE')
<button class="btn danger small" type="submit">Supprimer</button>
</form>
</td>
</tr>
@empty
<tr><td colspan="5" class="muted">Aucune affectation : tous les agents reçoivent le template par défaut.</td></tr>
@endforelse
</tbody>
</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 class="card card--flush">
<div class="card-head"><h2>Affectations, par ordre de priorité</h2></div>
<div class="table-wrap">
<table class="list">
<thead><tr><th>Priorité</th><th>Groupe AD</th><th>Membres</th><th>Template</th><th></th></tr></thead>
<tbody>
@forelse ($assignments as $a)
<tr>
<td>
<form class="row" method="post" action="{{ route('admin.assignments.update', $a) }}" style="flex-wrap:nowrap">
@csrf @method('PUT')
<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" title="Enregistrer la priorité" aria-label="Enregistrer la priorité"><x-icon name="check" :size="15" /></button>
</form>
</td>
<td>{{ $a->group?->display_name }}</td>
<td class="num">{{ $a->group?->members_count }}</td>
<td>
<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
</td>
<td class="cell-actions">
<form class="inline" method="post" action="{{ route('admin.assignments.destroy', $a) }}"
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')
<button class="btn danger small" type="submit"><x-icon name="trash-2" :size="15" />Supprimer</button>
</form>
</td>
</tr>
@empty
<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
</tbody>
</table>
</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>
@endsection
@@ -1,5 +1,5 @@
@extends('layouts.app')
@section('title', ($campaign->exists ? $campaign->name : 'Nouvelle campagne').' — Signatures CABM')
@section('title', ($campaign->exists ? $campaign->name : 'Nouvelle campagne').' — Signatures')
@section('content')
@php
$isAdmin = auth()->user()->hasRole('admin');
@@ -11,27 +11,34 @@
$checked = collect(old('groups', $selected))->map(fn ($id) => (int) $id)->all();
$status = $campaign->exists ? $campaign->status() : null;
@endphp
<div class="row" style="margin-bottom:16px">
<a href="{{ route('admin.campaigns.index') }}">← Campagnes</a>
<h1 style="margin:0 0 0 12px">{{ $campaign->exists ? $campaign->name : 'Nouvelle campagne' }}</h1>
@if ($status)
<span class="badge {{ ['running' => 'ok', 'inactive' => 'warn'][$status] ?? '' }}">{{ \App\Models\Campaign::STATUSES[$status] }}</span>
@endif
</div>
<header class="page-head">
<div>
<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)
<span class="badge {{ ['running' => 'ok', 'inactive' => 'warn', 'ended' => 'off'][$status] ?? '' }}">{{ \App\Models\Campaign::STATUSES[$status] }}</span>
@endif
</div>
</div>
</header>
@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
<form method="post" enctype="multipart/form-data"
action="{{ $campaign->exists ? route('admin.campaigns.update', $campaign) : route('admin.campaigns.store') }}">
@csrf
@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="card">
<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">
<label for="image_path">Image</label>
@@ -46,40 +53,45 @@
</option>
@endforeach
</select>
<label for="image_file">… ou envoyer une nouvelle image (PNG, JPEG, GIF)</label>
<input type="file" id="image_file" name="image_file" accept="image/png,image/jpeg,image/gif">
<p class="muted">Visez 600 px de large et moins de 100 Ko : les destinataires la téléchargent à chaque ouverture du message.</p>
<img id="thumb" alt="" hidden style="max-width:100%;max-height:160px;border:1px solid var(--line);background:#fff">
<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" aria-label="Nouvelle image">
<x-icon name="cloud-upload" :size="24" />
<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>
<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>
<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>
<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 class="card">
<h2>Diffusion</h2>
<div class="row">
<div style="flex:1;min-width:200px">
<label for="starts_at">Début</label>
<div class="field-row">
<div>
<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)) }}">
</div>
<div style="flex:1;min-width:200px">
<label for="ends_at">Fin</label>
<div>
<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)) }}">
</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>
<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>
<legend>Destinataires</legend>
@@ -98,18 +110,19 @@
@if ($isAdmin)
<input type="hidden" name="active" value="0">
<label class="check" style="margin-top:14px">
<input type="checkbox" name="active" value="1" @checked(old('active', $campaign->active))>Campagne active
<label class="switch" style="margin-top:16px">
<input type="checkbox" name="active" value="1" @checked(old('active', $campaign->active))>
<span class="switch-track"></span><strong>Campagne active</strong>
</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
<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
</div>
</div>
<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>
</div>
</fieldset>
@@ -117,9 +130,9 @@
@if ($campaign->exists)
<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>
@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>
@endif
@endsection
@@ -140,7 +153,7 @@
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', () => {
if (!file.files.length) return fromLibrary();
select.value = '';
@@ -1,100 +1,117 @@
@extends('layouts.app')
@section('title', 'Campagnes — Signatures CABM')
@section('title', 'Campagnes — Signatures')
@section('content')
@php
$isAdmin = auth()->user()->hasRole('admin');
$tz = config('signature.timezone');
$badge = ['running' => 'ok', 'scheduled' => '', 'inactive' => 'warn', 'ended' => ''];
$badge = ['running' => 'ok', 'scheduled' => '', 'inactive' => 'warn', 'ended' => 'off'];
$variable = '{'.'{'.'{banniere}'.'}'.'}';
$period = fn ($d) => $d->timezone($tz)->format('d/m/Y H:i');
@endphp
<div class="row" style="margin-bottom:8px">
<h1 style="margin:0">Campagnes</h1>
<span class="spacer"></span>
<a class="btn" href="{{ route('admin.campaigns.create') }}">Nouvelle campagne</a>
</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.
<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,
pas dans la signature courte des réponses, et seulement si le template contient <code>{{ $variable }}</code>.
@unless ($isAdmin) Vous préparez les campagnes ; un administrateur les active. @endunless
</p>
<header class="page-head">
<div>
<h1>Campagnes</h1>
<p class="lead">
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é
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.
@unless ($isAdmin) Vous préparez les campagnes ; un administrateur les active. @endunless
</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())
<div class="notice" role="note">
Ces templates publiés ne contiennent pas <code>{{ $variable }}</code> : leurs agents ne verront aucune bannière —
{{ $withoutBanner->join(', ') }}.
<div class="alert notice" role="note">
<x-icon name="triangle-alert" />
<div>Ces templates publiés ne contiennent pas <code>{{ $variable }}</code> : leurs agents ne verront aucune bannière.
<strong>{{ $withoutBanner->join(', ') }}</strong></div>
</div>
@endif
<div class="card" style="overflow-x:auto">
<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>
<tbody>
@forelse ($campaigns as $c)
@php
$status = $c->status();
$locked = $c->active && ! $isAdmin;
@endphp
<tr>
<td>
@if (in_array($c->id, $missing, true))
<span class="badge warn">image manquante</span>
<div class="muted">{{ $c->image_path }}</div>
@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">
@endif
</td>
<td>
<a href="{{ route('admin.campaigns.edit', $c) }}"><strong>{{ $c->name }}</strong></a>
<div class="muted">
@if ($c->link_url)
Lien : <a href="{{ $c->link_url }}" target="_blank" rel="noopener">{{ \Illuminate\Support\Str::limit($c->link_url, 50) }}</a>
<div class="card card--flush">
<div class="table-wrap">
<table class="list">
<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>
@forelse ($campaigns as $c)
@php
$status = $c->status();
$locked = $c->active && ! $isAdmin;
@endphp
<tr>
<td>
@if (in_array($c->id, $missing, true))
<span class="badge warn">image manquante</span>
<div class="muted">{{ $c->image_path }}</div>
@else
Sans lien
<img class="thumb" src="{{ asset('img/'.$c->image_path) }}" alt="{{ $c->alt_text }}" loading="lazy">
@endif
</div>
</td>
<td style="white-space:nowrap">{{ $period($c->starts_at) }}<br>→ {{ $period($c->ends_at) }}</td>
<td style="overflow-wrap:anywhere">{{ $c->groups->isEmpty() ? 'Tous les agents' : $c->groups->pluck('display_name')->join(', ') }}</td>
<td>{{ $c->priority }}</td>
<td>
<span class="badge {{ $badge[$status] }}">{{ \App\Models\Campaign::STATUSES[$status] }}</span>
@if ($status === 'running')
</td>
<td>
<a class="cell-title" href="{{ route('admin.campaigns.edit', $c) }}">{{ $c->name }}</a>
<div class="muted">
@if (($reach[$c->id] ?? 0) > 0)
vue par {{ $reach[$c->id] }} agent{{ $reach[$c->id] > 1 ? 's' : '' }}
@if ($c->link_url)
<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
aucun agent : masquée par une campagne plus prioritaire, ou groupes vides
Sans lien
@endif
</div>
@elseif ($status === 'inactive')
<div class="muted">{{ $isAdmin ? 'à activer' : 'en attente d\'un administrateur' }}</div>
@endif
</td>
<td style="text-align:right">
<a class="btn secondary small" style="margin:2px 0" href="{{ route('admin.campaigns.edit', $c) }}">{{ $locked ? 'Voir' : 'Modifier' }}</a>
@if ($isAdmin)
@if ($status !== 'ended')
<form class="inline" method="post" action="{{ route('admin.campaigns.toggle', $c) }}">
@csrf
<button class="btn secondary small" style="margin:2px 0" type="submit">{{ $c->active ? 'Désactiver' : 'Activer' }}</button>
</td>
<td class="nowrap">{{ $period($c->starts_at) }}<br><span class="muted">au</span> {{ $period($c->ends_at) }}</td>
<td style="min-width:130px;overflow-wrap:anywhere">{{ $c->groups->isEmpty() ? 'Tous les agents' : $c->groups->pluck('display_name')->join(', ') }}</td>
<td class="num">{{ $c->priority }}</td>
<td>
<span class="badge {{ $badge[$status] }}">{{ \App\Models\Campaign::STATUSES[$status] }}</span>
@if ($status === 'running')
<div class="muted">
@if (($reach[$c->id] ?? 0) > 0)
vue par {{ $reach[$c->id] }} agent{{ $reach[$c->id] > 1 ? 's' : '' }}
@else
aucun agent : masquée par une campagne plus prioritaire, ou groupes vides
@endif
</div>
@elseif ($status === 'inactive')
<div class="muted">{{ $isAdmin ? 'à activer' : 'en attente d\'un administrateur' }}</div>
@endif
</td>
<td class="cell-actions">
<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 ($status !== 'ended')
<form class="inline" method="post" action="{{ route('admin.campaigns.toggle', $c) }}">
@csrf
<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>
@endif
<form class="inline" method="post" action="{{ route('admin.campaigns.destroy', $c) }}"
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')
<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>
@endif
<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.')">
@csrf @method('DELETE')
<button class="btn danger small" style="margin:2px 0" type="submit">Supprimer</button>
</form>
@endif
</td>
</tr>
@empty
<tr><td colspan="7" class="muted">Aucune campagne. Les signatures sont envoyées sans bannière.</td></tr>
@endforelse
</tbody>
</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>
</td>
</tr>
@empty
<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
</tbody>
</table>
</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>
@endsection
@@ -1,55 +1,85 @@
@extends('layouts.app')
@section('title', 'Back-office — Signatures CABM')
@section('title', 'Tableau de bord — Signatures')
@section('content')
<div class="card">
<h1>{{ $tenant->name }}</h1>
<div class="row" style="margin-bottom:12px">
<p class="muted" style="margin:0">
Dernière synchro annuaire :
{{ $tenant->last_sync_at?->timezone('Europe/Paris')->format('d/m/Y H:i') ?? 'jamais' }}
— {{ $tenant->last_sync_status ?? 'n/a' }}
<br>Automatique toutes les 15 minutes (incrémentale) et chaque nuit (complète).
@php
$isAdmin = auth()->user()->hasRole('admin');
$syncOk = $tenant->last_sync_status === 'ok';
@endphp
<header class="page-head">
<div>
<h1>Tableau de bord</h1>
<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>
<span class="spacer"></span>
@if (auth()->user()->hasRole('admin'))
<form method="post" action="{{ route('admin.sync') }}" class="row" id="sync-form">
@csrf
<label class="muted" style="margin:0;font-weight:400">
<input type="checkbox" name="full" value="1"> complète
</label>
<button class="btn" type="submit" id="sync-btn">Synchroniser maintenant</button>
</form>
@endif
</div>
<div class="grid">
<div><div class="stat">{{ $users }}</div><div class="muted">agents synchronisés</div></div>
<div><div class="stat">{{ $groups->count() }}</div><div class="muted">groupes signature_*</div></div>
<div><div class="stat"><a href="{{ route('admin.templates.index') }}">{{ $templates }}</a></div><div class="muted">templates</div></div>
<div><div class="stat">{{ $assignments }}</div><div class="muted">affectations</div></div>
<div><div class="stat"><a href="{{ route('admin.campaigns.index') }}">{{ $campaigns }}</a></div><div class="muted">campagnes en cours</div></div>
<p class="muted">Automatique toutes les 15 minutes (incrémentale) et chaque nuit (complète).</p>
</div>
<span class="spacer"></span>
@if ($isAdmin)
<form method="post" action="{{ route('admin.sync') }}" class="row" data-busy="Synchronisation en cours…">
@csrf
<label class="switch">
<input type="checkbox" name="full" value="1"><span class="switch-track"></span>Synchronisation complète
</label>
<button class="btn" type="submit"><x-icon name="refresh-cw" /><span data-label>Synchroniser maintenant</span></button>
</form>
@endif
</div>
<div class="card">
<h2>Groupes synchronisés</h2>
<table class="list">
<thead><tr><th>Groupe</th><th>Membres (transitifs)</th></tr></thead>
<tbody>
@forelse ($groups as $g)
<tr><td>{{ $g->display_name }}</td><td>{{ $g->members_count }}</td></tr>
@empty
<tr><td colspan="2" class="muted">Aucun groupe : lancez « php artisan signature:sync --full ».</td></tr>
@endforelse
</tbody>
</table>
<div class="tiles">
<a class="tile" href="{{ route('admin.agents.index') }}">
<span class="tile-icon"><x-icon name="contact" :size="20" /></span>
<span><span class="tile-value">{{ $users }}</span><span class="tile-label">agents synchronisés</span></span>
</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>
@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 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">
<thead><tr><th>Groupe</th><th class="right">Membres (imbriqués inclus)</th></tr></thead>
<tbody>
@forelse ($groups as $g)
<tr><td>{{ $g->display_name }}</td><td class="right num">{{ $g->members_count }}</td></tr>
@empty
<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
</tbody>
</table>
</div>
</div>
@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')
@section('title', 'Champs agents — Signatures CABM')
@section('title', 'Champs agents — Signatures')
@section('content')
<h1>Champs modifiables par les agents</h1>
<p class="muted" style="margin-top:-8px">
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.
Publication immédiate, chaque modification est tracée. Fermer un champ fait revenir les signatures concernées à la valeur de l'annuaire.
</p>
<form method="post" action="{{ route('admin.fields.update') }}" class="card">
<header class="page-head">
<div>
<h1>Champs modifiables par les agents</h1>
<p class="lead">
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.
Publication immédiate, chaque modification est tracée. Fermer un champ fait revenir les signatures concernées à la valeur de l'annuaire.
</p>
</div>
</header>
<form method="post" action="{{ route('admin.fields.update') }}" class="card card--flush">
@csrf @method('PUT')
<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>
<tbody>
@foreach ($fields as $f)
<tr>
<td>
<input type="hidden" name="fields[{{ $f->field }}][enabled]" value="0">
<input type="checkbox" name="fields[{{ $f->field }}][enabled]" value="1" @checked($f->enabled) aria-label="Ouvrir {{ $f->label }}">
</td>
<td><code>{{ $f->field }}</code></td>
<td><input type="text" name="fields[{{ $f->field }}][label]" value="{{ $f->label }}" maxlength="60" required></td>
<td>
<select name="fields[{{ $f->field }}][type]">
@foreach ($types as $key => $type)
<option value="{{ $key }}" @selected(($f->validation_rule ?: 'text') === $key)>{{ $type['label'] }}</option>
@endforeach
</select>
</td>
<td class="muted">{{ $usage[$f->field] ?? 0 }}</td>
</tr>
@endforeach
</tbody>
</table>
<div class="row" style="margin-top:12px">
<button class="btn" type="submit">Enregistrer</button>
<div class="table-wrap">
<table class="list">
<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>
@foreach ($fields as $f)
<tr>
<td>
<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 }}">
<span class="switch-track"></span>
</label>
</td>
<td><code>{{ $f->field }}</code></td>
<td><input type="text" name="fields[{{ $f->field }}][label]" value="{{ $f->label }}" maxlength="60" required aria-label="Libellé de {{ $f->field }}"></td>
<td>
<select name="fields[{{ $f->field }}][type]" aria-label="Contrôle de {{ $f->field }}">
@foreach ($types as $key => $type)
<option value="{{ $key }}" @selected(($f->validation_rule ?: 'text') === $key)>{{ $type['label'] }}</option>
@endforeach
</select>
</td>
<td class="right num muted">{{ $usage[$f->field] ?? 0 }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<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>
</div>
</form>
@@ -1,79 +1,90 @@
@extends('layouts.app')
@section('title', 'Images — Signatures CABM')
@section('title', 'Images — Signatures')
@section('content')
@php
$tag = fn (string $inner) => '{'.'{'.$inner.'}'.'}';
$isAdmin = auth()->user()->hasRole('admin');
@endphp
<h1>Images</h1>
<p class="muted" style="margin-top:-8px">
Logos et bannières utilisés dans les 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).
</p>
<header class="page-head">
<div>
<h1>Images</h1>
<p class="lead">
Logos et bannières des signatures, publiés sur <code>{{ config('signature.public_url') }}/img/</code>.
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>
</div>
</header>
<div class="card">
<form method="post" action="{{ route('admin.images.store') }}" enctype="multipart/form-data" class="card" data-busy="Envoi…">
@csrf
<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
<div style="flex:2;min-width:240px">
<label for="image">Fichier (PNG, JPEG, GIF — {{ (int) ($maxKb / 1024) }} Mo max.)</label>
<input type="file" id="image" name="image" accept="image/png,image/jpeg,image/gif" required>
<div class="field-row" style="align-items:flex-end">
<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 style="flex:1;min-width:200px">
<label for="name">Nom (facultatif)</label>
<div>
<label for="name">Nom du fichier (facultatif)</label>
<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>
<button class="btn" type="submit">Envoyer</button>
</form>
</div>
</form>
<div class="card-head">
<h2>Images en ligne</h2>
<span class="badge plain">{{ $images->count() }}</span>
</div>
<div class="card">
<h2>Images en ligne ({{ $images->count() }})</h2>
<table class="list">
<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>
<tbody>
@forelse ($images as $img)
@if ($images->isEmpty())
<div class="card empty">
<x-icon name="images" :size="32" />
<p><strong>Aucune image</strong>Commencez par le logo : le template fourni par défaut attend un fichier nommé logo-cabm.png.</p>
</div>
@else
<div class="gallery">
@foreach ($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>
<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>
</td>
<td class="muted">{{ $img['used_in'] ? implode(', ', $img['used_in']) : '—' }}</td>
<td style="text-align:right">
<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)
<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')
<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>
@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
@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')
@section('title', ($template->exists ? $template->name : 'Nouveau template').' — Signatures CABM')
@section('title', ($template->exists ? $template->name : 'Nouveau template').' — Signatures')
@section('content')
@php
$isAdmin = auth()->user()->hasRole('admin');
@@ -15,28 +15,33 @@
];
@endphp
<div class="row" style="margin-bottom:16px">
<a href="{{ route('admin.templates.index') }}">← Templates</a>
<h1 style="margin:0 0 0 12px">{{ $template->exists ? $template->name : 'Nouveau template' }}</h1>
@if ($template->is_default) <span class="badge">par défaut</span> @endif
@if ($template->exists)
@if (! $template->published_version)
<span class="badge warn">jamais publié</span>
@elseif ($dirty)
<span class="badge warn">brouillon modifié depuis v{{ $template->published_version }}</span>
@else
<span class="badge ok">publié · v{{ $template->published_version }}</span>
@endif
@endif
<span class="spacer"></span>
<header class="page-head">
<div>
<a class="back" href="{{ route('admin.templates.index') }}"><x-icon name="chevron-left" :size="16" />Templates</a>
<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->published_version)
<span class="badge warn">jamais publié</span>
@elseif ($dirty)
<span class="badge warn">brouillon modifié depuis v{{ $template->published_version }}</span>
@else
<span class="badge ok">publié, v{{ $template->published_version }}</span>
@endif
@endif
</div>
</div>
@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>
<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
</div>
</header>
<form method="post" id="template-form"
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>
<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">
<button class="btn" type="submit">Enregistrer le brouillon</button>
<span class="muted">L'enregistrement ne change rien pour les agents tant que le template n'est pas publié.</span>
<div class="row" style="margin-top:14px">
<button class="btn" type="submit"><x-icon name="save" />Enregistrer le brouillon</button>
<span class="muted">Rien ne change pour les agents tant que le template n'est pas publié.</span>
</div>
</div>
<div>
<div class="card">
<div class="row">
<h2 style="margin:0">Aperçu</h2>
<div class="card-head">
<h2>Aperçu</h2>
<span class="spacer"></span>
<select id="preview-variant" aria-label="Variante affichée" style="width:auto">
<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['if'] }}" title="Ligne conditionnelle">#if …</button>
</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>
</div>
</div>
@@ -103,21 +108,22 @@
</form>
@if ($template->exists)
<div class="card">
<h2>Historique des publications</h2>
<div class="card card--flush">
<div class="card-head"><h2>Historique des publications</h2></div>
<div class="table-wrap">
<table class="list">
<thead><tr><th>Version</th><th>Publiée le</th><th>Par</th><th></th></tr></thead>
<tbody>
@forelse ($versions as $v)
<tr>
<td>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="num">v{{ $v->version }} @if ($v->version === $template->published_version) <span class="badge ok">en ligne</span> @endif</td>
<td class="nowrap">{{ $v->created_at->timezone('Europe/Paris')->format('d/m/Y H:i') }}</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]) }}"
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
<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>
</td>
</tr>
@@ -126,6 +132,7 @@
@endforelse
</tbody>
</table>
</div>
</div>
@if ($isAdmin)
@@ -133,13 +140,13 @@
@unless ($template->is_default)
<form class="inline" method="post" action="{{ route('admin.templates.default', $template) }}">
@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>
<span class="spacer"></span>
<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')
<button class="btn danger" type="submit">Supprimer</button>
<button class="btn danger" type="submit"><x-icon name="trash-2" />Supprimer</button>
</form>
@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>
@@ -1,39 +1,52 @@
@extends('layouts.app')
@section('title', 'Templates — Signatures CABM')
@section('title', 'Templates — Signatures')
@section('content')
<div class="row" style="margin-bottom:16px">
<h1 style="margin:0">Templates</h1>
<span class="spacer"></span>
<a class="btn" href="{{ route('admin.templates.create') }}">Nouveau template</a>
</div>
<div class="card">
<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>
<tbody>
@forelse ($templates as $t)
<tr>
<td>
<a href="{{ route('admin.templates.edit', $t) }}">{{ $t->name }}</a>
@if ($t->is_default) <span class="badge">par défaut</span> @endif
@if ($t->description)<div class="muted">{{ $t->description }}</div>@endif
</td>
<td>
@if (! $t->published_version)
<span class="badge warn">jamais publié</span>
@elseif ($t->draft_html_full !== $t->html_full || $t->draft_html_short !== $t->html_short)
<span class="badge warn">brouillon modifié</span>
@else
<span class="badge ok">publié</span>
@endif
</td>
<td>{{ $t->published_version ? 'v'.$t->published_version : '—' }}</td>
<td>{{ $t->assignments_count }}</td>
<td class="muted">{{ $t->updated_at?->timezone('Europe/Paris')->format('d/m/Y H:i') }}</td>
</tr>
@empty
<tr><td colspan="5" class="muted">Aucun template.</td></tr>
@endforelse
</tbody>
</table>
<header class="page-head">
<div>
<h1>Templates</h1>
<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 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">
<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>
@forelse ($templates as $t)
<tr>
<td>
<a class="cell-title" href="{{ route('admin.templates.edit', $t) }}">{{ $t->name }}</a>
@if ($t->is_default) <span class="badge plain">par défaut</span> @endif
@if ($t->description)<div class="muted">{{ $t->description }}</div>@endif
</td>
<td>
@if (! $t->published_version)
<span class="badge warn">jamais publié</span>
@elseif ($t->draft_html_full !== $t->html_full || $t->draft_html_short !== $t->html_short)
<span class="badge warn">brouillon modifié</span>
@else
<span class="badge ok">publié</span>
@endif
</td>
<td class="num">{{ $t->published_version ? 'v'.$t->published_version : '—' }}</td>
<td class="num">{{ $t->assignments_count }}</td>
<td class="muted nowrap">{{ $t->updated_at?->timezone('Europe/Paris')->format('d/m/Y H:i') }}</td>
</tr>
@empty
<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
</tbody>
</table>
</div>
</div>
@endsection