239 lines
13 KiB
PHP
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"> </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
|