45
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
|
||||
@source '../../storage/framework/views/*.php';
|
||||
|
||||
@theme {
|
||||
--font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
|
||||
'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
//
|
||||
@@ -0,0 +1,42 @@
|
||||
@extends('layouts.app')
|
||||
@section('title', 'Agents — Signatures CABM')
|
||||
@section('content')
|
||||
<h1>Agents</h1>
|
||||
<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>
|
||||
</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>
|
||||
@if ($agents->hasPages())
|
||||
<nav class="row" style="margin-top:12px" aria-label="Pagination">
|
||||
@if ($agents->onFirstPage())
|
||||
<span class="muted">← Précédent</span>
|
||||
@else
|
||||
<a href="{{ $agents->previousPageUrl() }}" rel="prev">← Précédent</a>
|
||||
@endif
|
||||
<span class="muted">Page {{ $agents->currentPage() }} / {{ $agents->lastPage() }}</span>
|
||||
@if ($agents->hasMorePages())
|
||||
<a href="{{ $agents->nextPageUrl() }}" rel="next">Suivant →</a>
|
||||
@else
|
||||
<span class="muted">Suivant →</span>
|
||||
@endif
|
||||
</nav>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,33 @@
|
||||
@extends('layouts.app')
|
||||
@section('title', $agent->display_name.' — Signatures CABM')
|
||||
@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>
|
||||
<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>
|
||||
@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>Synchronisé le</th><td class="muted">{{ $agent->synced_at?->timezone('Europe/Paris')->format('d/m/Y H:i') }}</td></tr>
|
||||
</table>
|
||||
</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'] ?? ''])
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,79 @@
|
||||
@extends('layouts.app')
|
||||
@section('title', 'Affectations — Signatures CABM')
|
||||
@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>
|
||||
|
||||
<div class="card">
|
||||
<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>
|
||||
<select id="directory_group_id" name="directory_group_id" required>
|
||||
<option value="">— choisir —</option>
|
||||
@if ($groups->isEmpty())
|
||||
<option value="" disabled>Aucun groupe synchronisé : lancez php artisan signature:sync --full</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>
|
||||
<select id="template_id" name="template_id" required>
|
||||
<option value="">— choisir —</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>
|
||||
<button class="btn" type="submit">Ajouter</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
@endsection
|
||||
@@ -0,0 +1,55 @@
|
||||
@extends('layouts.app')
|
||||
@section('title', 'Back-office — Signatures CABM')
|
||||
@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).
|
||||
</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">{{ $campaigns }}</div><div class="muted">campagnes en cours</div></div>
|
||||
</div>
|
||||
</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>
|
||||
@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
|
||||
@@ -0,0 +1,231 @@
|
||||
@extends('layouts.app')
|
||||
@section('title', ($template->exists ? $template->name : 'Nouveau template').' — Signatures CABM')
|
||||
@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
|
||||
|
||||
<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>
|
||||
@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>
|
||||
|
||||
<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: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>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<h2 style="margin:0">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 : déposez-les dans <code>public/img</code> 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">
|
||||
<h2>Historique des publications</h2>
|
||||
<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>{{ $v->publisher?->name ?? '—' }}</td>
|
||||
<td style="text-align:right">
|
||||
<form class="inline" method="post" action="{{ route('admin.templates.restore', [$template, $v]) }}"
|
||||
onsubmit="return confirm('Remplacer le brouillon actuel par la version {{ $v->version }} ?')">
|
||||
@csrf
|
||||
<button class="btn secondary small" type="submit">Recharger dans le brouillon</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="4" class="muted">Pas encore publié.</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</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)>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 ?')">
|
||||
@csrf @method('DELETE')
|
||||
<button class="btn danger" type="submit">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
|
||||
@@ -0,0 +1,39 @@
|
||||
@extends('layouts.app')
|
||||
@section('title', 'Templates — Signatures CABM')
|
||||
@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>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,84 @@
|
||||
<!doctype html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<title>@yield('title', 'Signatures CABM')</title>
|
||||
<style>
|
||||
:root { --ink:#1b1b1b; --muted:#5c5c5c; --line:#dcdcdc; --accent:#0b5cad; --accent-soft:#e7f0fa; --bg:#f6f7f9; --ok:#1f7a3d; --warn:#9a5b00; --danger:#b3261e; }
|
||||
* { box-sizing: border-box; }
|
||||
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], 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; }
|
||||
.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>
|
||||
<body>
|
||||
<header>
|
||||
<span class="brand">Signatures CABM</span>
|
||||
@auth
|
||||
@if (auth()->user()->role)
|
||||
<nav>
|
||||
<a href="{{ route('admin.dashboard') }}" @class(['active' => request()->routeIs('admin.dashboard')])>Tableau de bord</a>
|
||||
<a href="{{ route('admin.templates.index') }}" @class(['active' => request()->routeIs('admin.templates.*')])>Templates</a>
|
||||
@if (auth()->user()->hasRole('admin'))
|
||||
<a href="{{ route('admin.assignments.index') }}" @class(['active' => request()->routeIs('admin.assignments.*')])>Affectations</a>
|
||||
@endif
|
||||
<a href="{{ route('admin.agents.index') }}" @class(['active' => request()->routeIs('admin.agents.*')])>Agents</a>
|
||||
<a href="{{ route('portal.home') }}" @class(['active' => request()->routeIs('portal.*')])>Ma signature</a>
|
||||
</nav>
|
||||
@endif
|
||||
<form method="post" action="{{ route('logout') }}" class="row">@csrf
|
||||
<span class="muted">{{ auth()->user()->name }}@if (auth()->user()->role) · {{ auth()->user()->role }}@endif</span>
|
||||
<button class="link" type="submit">Se déconnecter</button>
|
||||
</form>
|
||||
@endauth
|
||||
</header>
|
||||
<main>
|
||||
@if (session('status'))
|
||||
<div class="flash" role="status">{{ session('status') }}</div>
|
||||
@endif
|
||||
@if ($errors->any())
|
||||
<div class="errors" role="alert">
|
||||
@foreach ($errors->all() as $error)<div>{{ $error }}</div>@endforeach
|
||||
</div>
|
||||
@endif
|
||||
@yield('content')
|
||||
</main>
|
||||
@stack('scripts')
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,3 @@
|
||||
{{-- 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
|
||||
srcdoc="{{ '<!doctype html><meta charset="utf-8"><base target="_blank"><body style="margin:12px">'.($html ?? '').'</body>' }}"></iframe>
|
||||
@@ -0,0 +1,13 @@
|
||||
@extends('layouts.app')
|
||||
@section('title', 'Ma signature — CABM')
|
||||
@section('content')
|
||||
<div class="card">
|
||||
<h1>Ma signature</h1>
|
||||
<p class="muted">Modèle : {{ $full['template'] ?? 'aucun' }}. Vos informations proviennent de l'annuaire de la collectivité ;
|
||||
la modification de certains champs arrivera prochainement.</p>
|
||||
<h2>Nouveau message</h2>
|
||||
@include('partials.signature-frame', ['html' => $full['html'] ?? '<em>Aucune signature</em>'])
|
||||
<h2 style="margin-top:16px">Réponse / transfert</h2>
|
||||
@include('partials.signature-frame', ['html' => ($reply['html'] ?? '') !== '' ? $reply['html'] : '<em>Aucune signature</em>'])
|
||||
</div>
|
||||
@endsection
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user