Integration Baniere dans le backend
This commit is contained in:
@@ -20,6 +20,14 @@
|
||||
<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>
|
||||
@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>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
@extends('layouts.app')
|
||||
@section('title', ($campaign->exists ? $campaign->name : 'Nouvelle campagne').' — Signatures CABM')
|
||||
@section('content')
|
||||
@php
|
||||
$isAdmin = auth()->user()->hasRole('admin');
|
||||
$tz = config('signature.timezone');
|
||||
$local = fn ($d) => $d?->timezone($tz)->format('Y-m-d\TH:i');
|
||||
$currentImage = old('image_path', $campaign->image_path);
|
||||
$currentImage = is_string($currentImage) ? $currentImage : null;
|
||||
$inLibrary = $images->contains('name', $currentImage);
|
||||
$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>
|
||||
|
||||
@if ($locked)
|
||||
<div class="notice" role="note">Cette campagne est active : seul un administrateur peut la modifier.</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">
|
||||
<div class="split">
|
||||
<div class="card">
|
||||
<h2>Bannière</h2>
|
||||
<label for="name">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>
|
||||
<select id="image_path" name="image_path">
|
||||
<option value="">— choisir dans la bibliothèque —</option>
|
||||
@if ($currentImage && ! $inLibrary)
|
||||
<option value="{{ $currentImage }}" selected>{{ $currentImage }} (introuvable)</option>
|
||||
@endif
|
||||
@foreach ($images as $img)
|
||||
<option value="{{ $img['name'] }}" data-src="{{ asset('img/'.$img['name']) }}" data-width="{{ $img['width'] }}" @selected($currentImage === $img['name'])>
|
||||
{{ $img['name'] }} — {{ $img['width'] }}×{{ $img['height'] }} px, {{ $img['kb'] }} Ko
|
||||
</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 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>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Diffusion</h2>
|
||||
<div class="row">
|
||||
<div style="flex:1;min-width:200px">
|
||||
<label for="starts_at">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>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<fieldset>
|
||||
<legend>Destinataires</legend>
|
||||
<p class="muted" style="margin:0 0 6px">Aucune case cochée : tous les agents. Sinon, seuls les membres des groupes cochés.</p>
|
||||
<div class="scroll">
|
||||
@forelse ($groups as $g)
|
||||
<label class="check">
|
||||
<input type="checkbox" name="groups[]" value="{{ $g->id }}" @checked(in_array($g->id, $checked, true))>{{ $g->display_name }}
|
||||
<span class="muted">({{ $g->members_count }})</span>
|
||||
</label>
|
||||
@empty
|
||||
<span class="muted">Aucun groupe synchronisé.</span>
|
||||
@endforelse
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
@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>
|
||||
<p class="muted">Décoché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>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<button class="btn" type="submit">{{ $campaign->exists ? 'Enregistrer' : 'Créer la campagne' }}</button>
|
||||
<a class="btn secondary" href="{{ route('admin.campaigns.index') }}">Annuler</a>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
@if ($campaign->exists)
|
||||
<div class="card" style="margin-top:16px">
|
||||
<h2>Rendu enregistré</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'])
|
||||
</div>
|
||||
@endif
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
(() => {
|
||||
const select = document.getElementById('image_path');
|
||||
const file = document.getElementById('image_file');
|
||||
const thumb = document.getElementById('thumb');
|
||||
const width = document.getElementById('width');
|
||||
|
||||
const show = (src) => { thumb.src = src; thumb.hidden = !src; };
|
||||
const fromLibrary = () => {
|
||||
const opt = select.selectedOptions[0];
|
||||
show(opt && opt.dataset.src ? opt.dataset.src : '');
|
||||
const natural = opt ? parseInt(opt.dataset.width, 10) : NaN;
|
||||
width.placeholder = natural ? `auto (${Math.min(natural, 600)})` : 'auto';
|
||||
};
|
||||
|
||||
select.addEventListener('change', () => { file.value = ''; fromLibrary(); });
|
||||
file.addEventListener('change', () => {
|
||||
if (!file.files.length) return fromLibrary();
|
||||
select.value = '';
|
||||
width.placeholder = 'auto';
|
||||
show(URL.createObjectURL(file.files[0]));
|
||||
});
|
||||
fromLibrary();
|
||||
})();
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,100 @@
|
||||
@extends('layouts.app')
|
||||
@section('title', 'Campagnes — Signatures CABM')
|
||||
@section('content')
|
||||
@php
|
||||
$isAdmin = auth()->user()->hasRole('admin');
|
||||
$tz = config('signature.timezone');
|
||||
$badge = ['running' => 'ok', 'scheduled' => '', 'inactive' => 'warn', 'ended' => ''];
|
||||
$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>
|
||||
|
||||
@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>
|
||||
@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>
|
||||
@else
|
||||
Sans lien
|
||||
@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')
|
||||
<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 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>
|
||||
</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>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -26,7 +26,7 @@
|
||||
<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 class="stat"><a href="{{ route('admin.campaigns.index') }}">{{ $campaigns }}</a></div><div class="muted">campagnes en cours</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
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; }
|
||||
input[type=text], input[type=number], input[type=search], input[type=url], input[type=datetime-local], select, textarea { width:100%; padding:8px 10px; border:1px solid var(--line); border-radius:6px; font:inherit; background:#fff; }
|
||||
textarea.code { font-family:Consolas, "Cascadia Mono", monospace; font-size:13px; line-height:1.45; min-height:260px; resize:vertical; tab-size:2; }
|
||||
input.small { width:90px; }
|
||||
.btn { display:inline-block; background:var(--accent); color:#fff; border:1px solid var(--accent); border-radius:6px; padding:7px 14px; font:inherit; font-size:14px; cursor:pointer; text-decoration:none; }
|
||||
@@ -42,6 +42,12 @@
|
||||
.badge.warn { background:#fdf1dc; color:var(--warn); }
|
||||
.flash { padding:10px 14px; border-radius:6px; margin-bottom:16px; background:#e5f4ea; color:var(--ok); border:1px solid #bfe3cb; }
|
||||
.errors { padding:10px 14px; border-radius:6px; margin-bottom:16px; background:#fde8e6; color:var(--danger); border:1px solid #f5c2bd; }
|
||||
.notice { padding:10px 14px; border-radius:6px; margin-bottom:16px; background:#fdf1dc; color:var(--warn); border:1px solid #f3d9a4; }
|
||||
label.check { font-weight:400; font-size:14px; margin:4px 0; }
|
||||
label.check input { margin-right:6px; }
|
||||
fieldset { border:1px solid var(--line); border-radius:6px; margin:12px 0 0; padding:8px 12px 12px; }
|
||||
legend { font-weight:600; font-size:13px; padding:0 4px; }
|
||||
.scroll { max-height:220px; overflow:auto; }
|
||||
.chips button { background:var(--accent-soft); color:var(--accent); border:0; border-radius:12px; padding:3px 9px; margin:2px; font:12px Consolas, monospace; cursor:pointer; }
|
||||
iframe.sig { width:100%; min-height:220px; border:1px solid var(--line); border-radius:6px; background:#fff; resize:vertical; }
|
||||
form.inline { display:inline; }
|
||||
@@ -56,6 +62,7 @@
|
||||
<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>
|
||||
<a href="{{ route('admin.images.index') }}" @class(['active' => request()->routeIs('admin.images.*')])>Images</a>
|
||||
<a href="{{ route('admin.campaigns.index') }}" @class(['active' => request()->routeIs('admin.campaigns.*')])>Campagnes</a>
|
||||
@if (auth()->user()->hasRole('admin'))
|
||||
<a href="{{ route('admin.assignments.index') }}" @class(['active' => request()->routeIs('admin.assignments.*')])>Affectations</a>
|
||||
<a href="{{ route('admin.fields.index') }}" @class(['active' => request()->routeIs('admin.fields.*')])>Champs agents</a>
|
||||
|
||||
Reference in New Issue
Block a user