Files
2026-10-02 13:06:14 +02:00

239 lines
13 KiB
PHP

@extends('layouts.app')
@section('title', ($template->exists ? $template->name : 'Nouveau template').' — Signatures')
@section('content')
@php
$isAdmin = auth()->user()->hasRole('admin');
$dirty = $template->exists && ($template->draft_html_full !== $template->html_full || $template->draft_html_short !== $template->html_short);
$tag = fn (string $inner) => '{'.'{'.$inner.'}'.'}';
$snippets = [
'banniere' => '{'.'{'.'{banniere}'.'}'.'}',
'site_url' => $tag('site_url'),
'if' => $tag('#if mobile').'Mob. '.$tag('mobile').'<br>'.$tag('/if'),
'if_open' => $tag('#if variable'),
'if_close' => $tag('/if'),
'img' => $tag('site_url').'/img/fichier.png',
];
@endphp
<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)
<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"
action="{{ $template->exists ? route('admin.templates.update', $template) : route('admin.templates.store') }}">
@csrf
@if ($template->exists) @method('PUT') @endif
<div class="split">
<div class="card">
<label for="name">Nom</label>
<input type="text" id="name" name="name" required maxlength="120" value="{{ old('name', $template->name) }}">
<label for="description">Description</label>
<input type="text" id="description" name="description" maxlength="500" value="{{ old('description', $template->description) }}">
<label for="draft_html_full">Signature complète (nouveaux messages, invitations)</label>
<textarea class="code" id="draft_html_full" name="draft_html_full" data-variant="full" spellcheck="false" required>{{ old('draft_html_full', $template->draft_html_full) }}</textarea>
<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: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="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>
<option value="short">Courte</option>
</select>
</div>
<label for="agent-search">Agent utilisé pour l'aperçu</label>
<input type="search" id="agent-search" list="agent-options" autocomplete="off"
placeholder="Nom ou adresse (2 caractères min.)"
value="{{ $previewAgent ? $previewAgent->display_name.' — '.$previewAgent->mail : '' }}">
<datalist id="agent-options"></datalist>
<input type="hidden" id="agent-id" value="{{ $previewAgent?->id }}">
<p class="muted" id="preview-status" aria-live="polite">&nbsp;</p>
@include('partials.signature-frame', ['id' => 'preview', 'html' => ''])
</div>
<div class="card">
<h2>Variables</h2>
<p class="muted">Cliquez pour insérer à l'emplacement du curseur. Une ligne entourée de
<code>{{ $snippets['if_open'] }}…{{ $snippets['if_close'] }}</code> disparaît quand le champ est vide.</p>
<div class="chips" id="variables">
@foreach ($variables as $key => $def)
<button type="button" data-insert="{{ $tag($key) }}" title="{{ $def['label'] }}">{{ $key }}</button>
@endforeach
<button type="button" data-insert="{{ $snippets['banniere'] }}" title="Bannière de campagne">banniere</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>
</div>
<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>
</div>
</form>
@if ($template->exists)
<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 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 class="cell-actions">
<form class="inline" method="post" action="{{ route('admin.templates.restore', [$template, $v]) }}"
data-confirm="Remplacer le brouillon actuel par la version {{ $v->version }} ?" data-confirm-label="Recharger">
@csrf
<button class="btn secondary small" type="submit"><x-icon name="rotate-ccw" :size="15" />Recharger dans le brouillon</button>
</form>
</td>
</tr>
@empty
<tr><td colspan="4" class="muted">Pas encore publié.</td></tr>
@endforelse
</tbody>
</table>
</div>
</div>
@if ($isAdmin)
<div class="card row">
@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)><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) }}"
data-confirm="Supprimer ce template et ses affectations ?" data-confirm-label="Supprimer" data-confirm-danger>
@csrf @method('DELETE')
<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>
@endunless
</div>
@endif
@endif
@endsection
@push('scripts')
<script>
(() => {
const csrf = document.querySelector('meta[name="csrf-token"]').content;
const frame = document.getElementById('preview');
const status = document.getElementById('preview-status');
const variantSel = document.getElementById('preview-variant');
const agentId = document.getElementById('agent-id');
const agentSearch = document.getElementById('agent-search');
const options = document.getElementById('agent-options');
const areas = { full: document.getElementById('draft_html_full'), short: document.getElementById('draft_html_short') };
let lastFocused = areas.full, timer = null, found = [];
const shell = (html) => '<!doctype html><meta charset="utf-8"><body style="margin:12px">' + html + '</body>';
async function refresh() {
const variant = variantSel.value;
const html = areas[variant].value || (variant === 'short' ? areas.full.value : '');
status.textContent = 'Mise à jour…';
try {
const res = await fetch(@json(route('admin.templates.preview')), {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-CSRF-TOKEN': csrf },
body: JSON.stringify({ html, variant, agent_id: agentId.value || null }),
});
if (!res.ok) throw new Error(res.status);
const data = await res.json();
frame.srcdoc = shell(data.html);
status.textContent = 'Aperçu pour ' + data.agent + (variant === 'short' && !areas.short.value ? ' (variante courte vide : la complète sera utilisée)' : '');
} catch (e) {
status.textContent = 'Aperçu indisponible (' + e.message + ').';
}
}
const schedule = () => { clearTimeout(timer); timer = setTimeout(refresh, 400); };
Object.values(areas).forEach((ta) => {
ta.addEventListener('input', schedule);
ta.addEventListener('focus', () => {
lastFocused = ta;
if (variantSel.value !== ta.dataset.variant) { variantSel.value = ta.dataset.variant; refresh(); }
});
// Tabulation dans l'éditeur au lieu de changer de champ
ta.addEventListener('keydown', (e) => {
if (e.key !== 'Tab') return;
e.preventDefault();
ta.setRangeText(' ', ta.selectionStart, ta.selectionEnd, 'end');
schedule();
});
});
variantSel.addEventListener('change', refresh);
document.getElementById('variables').addEventListener('click', (e) => {
const text = e.target.dataset && e.target.dataset.insert;
if (!text) return;
lastFocused.focus();
lastFocused.setRangeText(text, lastFocused.selectionStart, lastFocused.selectionEnd, 'end');
schedule();
});
agentSearch.addEventListener('input', async () => {
const q = agentSearch.value.trim();
const match = found.find((a) => a.label === q);
if (match) { agentId.value = match.id; refresh(); return; }
if (q.length < 2) return;
const res = await fetch(@json(route('admin.agents.lookup')) + '?q=' + encodeURIComponent(q), { headers: { 'Accept': 'application/json' } });
if (!res.ok) return;
found = await res.json();
options.replaceChildren(...found.map((a) => Object.assign(document.createElement('option'), { value: a.label })));
});
// Prévenir la perte de modifications non enregistrées
let changed = false;
document.getElementById('template-form').addEventListener('input', () => { changed = true; });
document.getElementById('template-form').addEventListener('submit', () => { changed = false; });
window.addEventListener('beforeunload', (e) => { if (changed) { e.preventDefault(); e.returnValue = ''; } });
refresh();
})();
</script>
@endpush