oups
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
{{define "title"}}Audit Log{{end}}
|
||||
{{define "header"}}Audit Log{{end}}
|
||||
{{define "content"}}
|
||||
|
||||
<!-- Filters -->
|
||||
<form method="GET" action="/audit" class="bg-discord-card rounded-lg border border-discord-border p-4 mb-6">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-3 mb-3">
|
||||
<div>
|
||||
<label class="block text-xs text-gray-400 mb-1">Admin ID (interne)</label>
|
||||
<input type="text" name="admin_id" value="{{.FAdminID}}" placeholder="ex: 1"
|
||||
class="w-full bg-discord-hover border border-discord-border rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-discord-accent">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs text-gray-400 mb-1">Action</label>
|
||||
<select name="action" class="w-full bg-discord-hover border border-discord-border rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:border-discord-accent">
|
||||
<option value="">Toutes les actions</option>
|
||||
{{range .Actions}}
|
||||
<option value="{{.}}" {{if eq . $.FAction}}selected{{end}}>{{.}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs text-gray-400 mb-1">Du</label>
|
||||
<input type="date" name="from" value="{{.FFrom}}"
|
||||
class="w-full bg-discord-hover border border-discord-border rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:border-discord-accent">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs text-gray-400 mb-1">Au</label>
|
||||
<input type="date" name="to" value="{{.FTo}}"
|
||||
class="w-full bg-discord-hover border border-discord-border rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:border-discord-accent">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<button type="submit"
|
||||
class="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white text-sm font-medium rounded-lg transition-colors">
|
||||
Filtrer
|
||||
</button>
|
||||
<a href="/audit" class="px-4 py-2 text-sm text-gray-400 hover:text-white border border-discord-border rounded-lg transition-colors">
|
||||
Réinitialiser
|
||||
</a>
|
||||
<span class="text-xs text-gray-500 ml-auto">{{.Total}} entrée{{if gt .Total 1}}s{{end}}</span>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Audit table -->
|
||||
<div class="bg-discord-card rounded-lg border border-discord-border overflow-hidden">
|
||||
{{if .Entries}}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="text-xs text-gray-400 uppercase border-b border-discord-border">
|
||||
<th class="px-4 py-2 text-left">Date</th>
|
||||
<th class="px-4 py-2 text-left">Admin ID</th>
|
||||
<th class="px-4 py-2 text-left">Action</th>
|
||||
<th class="px-4 py-2 text-left">Entité</th>
|
||||
<th class="px-4 py-2 text-left">Entité ID</th>
|
||||
<th class="px-4 py-2 text-left">IP</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-discord-border">
|
||||
{{range .Entries}}
|
||||
<tr class="hover:bg-white/5 transition-colors">
|
||||
<td class="px-4 py-2 text-gray-400 text-xs whitespace-nowrap">{{.CreatedAt.Format "02/01/2006 15:04:05"}}</td>
|
||||
<td class="px-4 py-2 text-gray-300 font-mono text-xs">{{.AdminID}}</td>
|
||||
<td class="px-4 py-2">
|
||||
<span class="px-2 py-0.5 rounded text-xs font-mono bg-discord-hover text-indigo-300 border border-discord-border">{{.Action}}</span>
|
||||
</td>
|
||||
<td class="px-4 py-2 text-gray-400 text-xs">{{.EntityType}}</td>
|
||||
<td class="px-4 py-2 text-gray-400 text-xs font-mono">
|
||||
{{if .EntityID.Valid}}{{.EntityID.Int64}}{{else}}—{{end}}
|
||||
</td>
|
||||
<td class="px-4 py-2 text-gray-500 text-xs font-mono">{{.IPAddress}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{if gt .Pages 1}}
|
||||
<div class="px-5 py-3 border-t border-discord-border flex items-center gap-2 text-sm">
|
||||
{{if gt .Page 1}}
|
||||
<a href="?page={{add .Page -1}}&admin_id={{.FAdminID}}&action={{.FAction}}&from={{.FFrom}}&to={{.FTo}}"
|
||||
class="px-3 py-1 rounded border border-discord-border text-gray-300 hover:text-white hover:border-gray-500 transition-colors">← Précédent</a>
|
||||
{{end}}
|
||||
<span class="text-gray-400">Page {{.Page}} / {{.Pages}}</span>
|
||||
{{if lt .Page .Pages}}
|
||||
<a href="?page={{add .Page 1}}&admin_id={{.FAdminID}}&action={{.FAction}}&from={{.FFrom}}&to={{.FTo}}"
|
||||
class="px-3 py-1 rounded border border-discord-border text-gray-300 hover:text-white hover:border-gray-500 transition-colors">Suivant →</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{else}}
|
||||
<div class="px-5 py-12 text-center text-gray-500 text-sm">
|
||||
Aucune entrée d'audit pour ces critères.
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,28 @@
|
||||
{{define "auth_layout"}}
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr" class="h-full">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{block "title" .}}TicketBot Admin{{end}}</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="/static/app.css">
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: { extend: { colors: { discord: { bg: '#1e1f22', card: '#2b2d31', accent: '#5865f2', hover: '#404249', border: '#3f4147' } } } }
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="h-full bg-discord-bg text-gray-100 flex items-center justify-center p-4">
|
||||
<div class="w-full max-w-md">
|
||||
<div class="text-center mb-8">
|
||||
<h1 class="text-2xl font-bold text-white">TicketBot</h1>
|
||||
<p class="text-gray-400 text-sm mt-1">Panel d'administration</p>
|
||||
</div>
|
||||
<div class="bg-discord-card rounded-lg shadow-xl p-8 border border-discord-border">
|
||||
{{block "content" .}}{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
@@ -0,0 +1,158 @@
|
||||
{{define "title"}}Convocations{{end}}
|
||||
{{define "header"}}Convocations{{end}}
|
||||
{{define "content"}}
|
||||
|
||||
<!-- Config form -->
|
||||
<div class="bg-discord-card rounded-lg border border-discord-border p-5 mb-6">
|
||||
<h3 class="font-semibold text-white mb-4">Configuration des convocations</h3>
|
||||
<form method="POST" action="/convocations" class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
||||
|
||||
<div>
|
||||
<label class="block text-xs text-gray-400 mb-1">ID de catégorie Discord (pour les salons)</label>
|
||||
<input type="text" name="category_id" value="{{if .Config}}{{.Config.CategoryID}}{{end}}"
|
||||
placeholder="ex: 123456789012345678"
|
||||
class="w-full bg-discord-hover border border-discord-border rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-discord-accent">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs text-gray-400 mb-1">ID du salon de logs</label>
|
||||
<input type="text" name="log_channel_id" value="{{if .Config}}{{.Config.LogChannelID}}{{end}}"
|
||||
placeholder="ex: 123456789012345678"
|
||||
class="w-full bg-discord-hover border border-discord-border rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-discord-accent">
|
||||
</div>
|
||||
|
||||
<div class="sm:col-span-2 flex items-center gap-3">
|
||||
<label class="flex items-center gap-2 cursor-pointer select-none">
|
||||
<input type="hidden" name="modal_enabled" value="0">
|
||||
<input type="checkbox" name="modal_enabled" value="1"
|
||||
{{if and .Config .Config.ModalEnabled}}checked{{end}}
|
||||
class="w-4 h-4 rounded accent-indigo-500">
|
||||
<span class="text-sm text-gray-300">Activer le modal (titre + description)</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="sm:col-span-2 border-t border-discord-border pt-4 mt-1">
|
||||
<p class="text-xs text-gray-400 uppercase font-semibold tracking-wide mb-3">Panel Discord (bouton convocation)</p>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-xs text-gray-400 mb-1">Salon d'envoi du panel</label>
|
||||
<input type="text" name="panel_channel_id" value="{{if .Config}}{{.Config.PanelChannelID}}{{end}}"
|
||||
placeholder="ex: 123456789012345678"
|
||||
class="w-full bg-discord-hover border border-discord-border rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-discord-accent">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-400 mb-1">Couleur de l'embed</label>
|
||||
<input type="color" name="panel_embed_color" value="{{if and .Config .Config.PanelEmbedColor}}{{.Config.PanelEmbedColor}}{{else}}#5865f2{{end}}"
|
||||
class="h-10 w-full rounded cursor-pointer bg-discord-hover border border-discord-border">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-400 mb-1">Titre de l'embed</label>
|
||||
<input type="text" name="panel_embed_title" value="{{if .Config}}{{.Config.PanelEmbedTitle}}{{else}}Créer une convocation{{end}}"
|
||||
class="w-full bg-discord-hover border border-discord-border rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-discord-accent"
|
||||
placeholder="Créer une convocation">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-400 mb-1">Description de l'embed</label>
|
||||
<textarea name="panel_embed_description" rows="2"
|
||||
class="w-full bg-discord-hover border border-discord-border rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-discord-accent resize-none"
|
||||
placeholder="Cliquez sur le bouton pour ouvrir une convocation...">{{if .Config}}{{.Config.PanelEmbedDescription}}{{end}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sm:col-span-2 flex items-center gap-3 justify-end">
|
||||
<button type="submit"
|
||||
class="px-4 py-2 bg-gray-700 hover:bg-gray-600 text-white text-sm font-medium rounded-lg transition-colors">
|
||||
Sauvegarder
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Send panel button (separate form) -->
|
||||
{{if and .Config .Config.PanelChannelID}}
|
||||
<div class="mt-3 pt-3 border-t border-discord-border flex items-center gap-3">
|
||||
<form method="POST" action="/convocations/send-panel">
|
||||
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
||||
<button type="submit"
|
||||
class="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white text-sm font-medium rounded-lg transition-colors">
|
||||
Envoyer le panel sur Discord
|
||||
</button>
|
||||
</form>
|
||||
{{if and .Config .Config.PanelMessageID}}
|
||||
<span class="text-xs text-green-400 bg-green-900/30 px-2 py-1 rounded">Panel envoyé</span>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<!-- Convocation list -->
|
||||
<div class="bg-discord-card rounded-lg border border-discord-border overflow-hidden">
|
||||
<div class="px-5 py-4 border-b border-discord-border flex items-center justify-between">
|
||||
<h3 class="font-semibold text-white">Convocations passées</h3>
|
||||
<span class="text-xs text-gray-400">{{.Total}} au total</span>
|
||||
</div>
|
||||
|
||||
{{if .Tickets}}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="text-xs text-gray-400 uppercase border-b border-discord-border">
|
||||
<th class="px-4 py-2 text-left">#</th>
|
||||
<th class="px-4 py-2 text-left">Utilisateur</th>
|
||||
<th class="px-4 py-2 text-left">Titre</th>
|
||||
<th class="px-4 py-2 text-left">Staff</th>
|
||||
<th class="px-4 py-2 text-left">Statut</th>
|
||||
<th class="px-4 py-2 text-left">Ouvert le</th>
|
||||
<th class="px-4 py-2 text-left">Fermé le</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-discord-border">
|
||||
{{range .Tickets}}
|
||||
<tr class="hover:bg-white/5 transition-colors">
|
||||
<td class="px-4 py-2 text-indigo-400 font-mono">{{.TicketNumber}}</td>
|
||||
<td class="px-4 py-2 text-gray-300 font-mono text-xs">{{.UserID}}</td>
|
||||
<td class="px-4 py-2 text-white">
|
||||
{{if .TicketTitle.Valid}}{{.TicketTitle.String}}{{else}}<span class="text-gray-500">—</span>{{end}}
|
||||
</td>
|
||||
<td class="px-4 py-2 text-gray-400 font-mono text-xs">
|
||||
{{if .ClaimedBy.Valid}}{{.ClaimedBy.String}}{{else}}—{{end}}
|
||||
</td>
|
||||
<td class="px-4 py-2">
|
||||
{{if eq .Status "open"}}
|
||||
<span class="px-2 py-0.5 rounded text-xs bg-green-900/40 text-green-400 border border-green-800">Ouvert</span>
|
||||
{{else if eq .Status "claimed"}}
|
||||
<span class="px-2 py-0.5 rounded text-xs bg-yellow-900/40 text-yellow-400 border border-yellow-800">Claimé</span>
|
||||
{{else}}
|
||||
<span class="px-2 py-0.5 rounded text-xs bg-gray-700 text-gray-400">Fermé</span>
|
||||
{{end}}
|
||||
</td>
|
||||
<td class="px-4 py-2 text-gray-400 text-xs">{{.OpenedAt.Format "02/01/2006 15:04"}}</td>
|
||||
<td class="px-4 py-2 text-gray-400 text-xs">
|
||||
{{if .ClosedAt.Valid}}{{.ClosedAt.Time.Format "02/01/2006 15:04"}}{{else}}—{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{if gt .Pages 1}}
|
||||
<div class="px-5 py-3 border-t border-discord-border flex items-center gap-2 text-sm">
|
||||
{{if gt .Page 1}}
|
||||
<a href="?page={{add .Page -1}}" class="px-3 py-1 rounded border border-discord-border text-gray-300 hover:text-white hover:border-gray-500 transition-colors">← Précédent</a>
|
||||
{{end}}
|
||||
<span class="text-gray-400">Page {{.Page}} / {{.Pages}}</span>
|
||||
{{if lt .Page .Pages}}
|
||||
<a href="?page={{add .Page 1}}" class="px-3 py-1 rounded border border-discord-border text-gray-300 hover:text-white hover:border-gray-500 transition-colors">Suivant →</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{else}}
|
||||
<div class="px-5 py-12 text-center text-gray-500 text-sm">
|
||||
Aucune convocation enregistrée.
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,140 @@
|
||||
{{define "title"}}Dashboard{{end}}
|
||||
{{define "header"}}Dashboard{{end}}
|
||||
{{define "content"}}
|
||||
<!-- Stat cards -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4 mb-6">
|
||||
<div class="bg-discord-card rounded-lg p-5 border border-discord-border">
|
||||
<p class="text-xs text-gray-400 uppercase tracking-wide mb-1">Tickets ouverts</p>
|
||||
<p class="text-3xl font-bold text-white">{{.Stats.OpenTickets}}</p>
|
||||
</div>
|
||||
<div class="bg-discord-card rounded-lg p-5 border border-discord-border">
|
||||
<p class="text-xs text-gray-400 uppercase tracking-wide mb-1">Fermés (30j)</p>
|
||||
<p class="text-3xl font-bold text-white">{{.Stats.ClosedLast30}}</p>
|
||||
</div>
|
||||
<div class="bg-discord-card rounded-lg p-5 border border-discord-border">
|
||||
<p class="text-xs text-gray-400 uppercase tracking-wide mb-1">Moy. claim (30j)</p>
|
||||
<p class="text-3xl font-bold text-white">
|
||||
{{if .Stats.AvgClaimMinutes}}{{.Stats.AvgClaimMinutes}}<span class="text-lg text-gray-400">min</span>{{else}}—{{end}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="bg-discord-card rounded-lg p-5 border border-discord-border">
|
||||
<p class="text-xs text-gray-400 uppercase tracking-wide mb-1">Moy. résolution (30j)</p>
|
||||
<p class="text-3xl font-bold text-white">
|
||||
{{if .Stats.AvgResolutionMinutes}}{{.Stats.AvgResolutionMinutes}}<span class="text-lg text-gray-400">min</span>{{else}}—{{end}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chart + Bot status -->
|
||||
<div class="grid grid-cols-1 xl:grid-cols-4 gap-6 mb-6">
|
||||
<!-- Chart tickets / jour -->
|
||||
<div class="xl:col-span-3 bg-discord-card rounded-lg p-5 border border-discord-border">
|
||||
<h3 class="font-semibold text-white mb-4">Tickets ouverts — 30 derniers jours</h3>
|
||||
{{if .Chart}}
|
||||
<div class="flex items-end gap-1 h-32">
|
||||
{{range .Chart}}
|
||||
<div class="flex-1 flex flex-col items-center gap-1 group relative">
|
||||
<div class="w-full rounded-t bg-indigo-500/70 hover:bg-indigo-400 transition-colors"
|
||||
style="height: {{if $.ChartMax}}{{percent .Count $.ChartMax}}%{{else}}0%{{end}}; min-height: {{if .Count}}2px{{else}}0{{end}}">
|
||||
</div>
|
||||
<span class="text-[9px] text-gray-500 rotate-45 origin-left whitespace-nowrap">{{.Day}}</span>
|
||||
<span class="absolute bottom-8 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-xs px-1.5 py-0.5 rounded opacity-0 group-hover:opacity-100 pointer-events-none">{{.Count}}</span>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<p class="text-gray-500 text-sm">Pas encore de données.</p>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<!-- Bot status -->
|
||||
<div class="bg-discord-card rounded-lg p-5 border border-discord-border">
|
||||
<h3 class="font-semibold text-white mb-4">Statut du bot</h3>
|
||||
<dl class="space-y-2 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-gray-400">Connexion</dt>
|
||||
<dd class="{{if .BotOnline}}text-green-400{{else}}text-red-400{{end}} font-medium flex items-center gap-1">
|
||||
<span class="w-2 h-2 rounded-full {{if .BotOnline}}bg-green-400{{else}}bg-red-400{{end}}"></span>
|
||||
{{if .BotOnline}}En ligne{{else}}Hors ligne{{end}}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-gray-400">Uptime</dt>
|
||||
<dd class="text-white">{{.Uptime}}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-gray-400">Latence</dt>
|
||||
<dd class="text-white">{{if .LatencyMs}}{{.LatencyMs}}ms{{else}}—{{end}}</dd>
|
||||
</div>
|
||||
{{if .GuildName}}
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-gray-400">Serveur</dt>
|
||||
<dd class="text-white truncate max-w-[120px]">{{.GuildName}}</dd>
|
||||
</div>
|
||||
{{end}}
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Staff table + Panels -->
|
||||
<div class="grid grid-cols-1 xl:grid-cols-3 gap-6">
|
||||
<!-- Staff stats -->
|
||||
<div class="xl:col-span-2 bg-discord-card rounded-lg border border-discord-border overflow-hidden">
|
||||
<div class="px-5 py-4 border-b border-discord-border">
|
||||
<h3 class="font-semibold text-white">Tickets par staff (30j)</h3>
|
||||
</div>
|
||||
{{if .Staff}}
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="text-xs text-gray-400 uppercase">
|
||||
<th class="px-5 py-2 text-left">Staff</th>
|
||||
<th class="px-4 py-2 text-right">Claimés</th>
|
||||
<th class="px-4 py-2 text-right">Fermés</th>
|
||||
<th class="px-4 py-2 text-right">Moy. claim</th>
|
||||
<th class="px-4 py-2 text-right">Moy. résol.</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-discord-border">
|
||||
{{range .Staff}}
|
||||
<tr class="hover:bg-white/5 transition-colors">
|
||||
<td class="px-5 py-3 text-white font-mono text-xs">{{.StaffID}}</td>
|
||||
<td class="px-4 py-3 text-right text-indigo-400 font-semibold">{{.Claimed}}</td>
|
||||
<td class="px-4 py-3 text-right text-gray-300">{{.Closed}}</td>
|
||||
<td class="px-4 py-3 text-right text-gray-400">{{fmtMin .AvgClaimMinutes}}</td>
|
||||
<td class="px-4 py-3 text-right text-gray-400">{{fmtMin .AvgResolutionMinutes}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
<p class="px-5 py-8 text-gray-500 text-sm text-center">Aucun ticket claimé sur les 30 derniers jours.</p>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<!-- Panels actifs -->
|
||||
<div class="bg-discord-card rounded-lg border border-discord-border overflow-hidden">
|
||||
<div class="px-5 py-4 border-b border-discord-border flex items-center justify-between">
|
||||
<h3 class="font-semibold text-white">Panels</h3>
|
||||
<a href="/panels/new" class="text-xs text-indigo-400 hover:text-indigo-300">+ Créer</a>
|
||||
</div>
|
||||
{{if .Panels}}
|
||||
<ul class="divide-y divide-discord-border">
|
||||
{{range .Panels}}
|
||||
<li class="px-5 py-3 flex items-center justify-between text-sm">
|
||||
<span class="flex items-center gap-2">
|
||||
<span class="w-3 h-3 rounded-full flex-shrink-0" style="background-color:{{.EmbedColor}}"></span>
|
||||
<span class="text-white font-medium">{{.Name}}</span>
|
||||
<span class="text-gray-500">{{len .Types}}</span>
|
||||
</span>
|
||||
<a href="/panels/{{.ID}}/edit" class="text-indigo-400 hover:text-indigo-300">Éditer →</a>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{else}}
|
||||
<p class="px-5 py-8 text-gray-500 text-sm text-center">
|
||||
<a href="/panels/new" class="text-indigo-400 hover:text-indigo-300">Créer un panel →</a>
|
||||
</p>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,13 @@
|
||||
{{define "content"}}
|
||||
<div class="text-center py-4">
|
||||
<div class="text-5xl mb-4">🚫</div>
|
||||
<h2 class="text-xl font-semibold text-white mb-2">Accès refusé</h2>
|
||||
<p class="text-gray-400 text-sm mb-6">
|
||||
Ton compte Discord n'est pas autorisé à accéder à ce panel.<br>
|
||||
Contacte un administrateur pour obtenir l'accès.
|
||||
</p>
|
||||
<a href="/login" class="text-indigo-400 hover:text-indigo-300 text-sm transition-colors">
|
||||
← Retour à la connexion
|
||||
</a>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,125 @@
|
||||
{{define "layout"}}
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr" class="h-full">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
{{if .CSRFToken}}<meta name="csrf-token" content="{{.CSRFToken}}">{{end}}
|
||||
<title>{{block "title" .}}Dashboard{{end}} — TicketBot Admin</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
|
||||
<link rel="stylesheet" href="/static/app.css">
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: { extend: { colors: { discord: { bg: '#1e1f22', card: '#2b2d31', accent: '#5865f2', hover: '#404249', border: '#3f4147', sidebar: '#1e1f22' } } } }
|
||||
}
|
||||
// HTMX CSRF
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.body.addEventListener('htmx:configRequest', function(e) {
|
||||
const meta = document.querySelector('meta[name="csrf-token"]');
|
||||
if (meta) e.detail.headers['X-CSRF-Token'] = meta.content;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body class="h-full bg-discord-bg text-gray-100 flex overflow-hidden">
|
||||
|
||||
<!-- Sidebar -->
|
||||
<aside class="w-60 flex-shrink-0 bg-discord-sidebar flex flex-col h-screen border-r border-discord-border">
|
||||
<div class="p-4 border-b border-discord-border">
|
||||
<span class="font-bold text-white text-lg">TicketBot</span>
|
||||
<span class="text-xs text-gray-400 block">Admin Panel</span>
|
||||
</div>
|
||||
|
||||
<nav class="flex-1 overflow-y-auto p-2 space-y-1">
|
||||
<a href="/" class="sidebar-link flex items-center gap-3 px-3 py-2 rounded text-gray-300 hover:bg-discord-hover hover:text-white transition-colors {{if eq .ActivePage "dashboard"}}sidebar-link-active{{end}}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/></svg>
|
||||
Dashboard
|
||||
</a>
|
||||
<a href="/panels" class="sidebar-link flex items-center gap-3 px-3 py-2 rounded text-gray-300 hover:bg-discord-hover hover:text-white transition-colors {{if eq .ActivePage "panels"}}sidebar-link-active{{end}}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"/></svg>
|
||||
Panels
|
||||
</a>
|
||||
<a href="/convocations" class="sidebar-link flex items-center gap-3 px-3 py-2 rounded text-gray-300 hover:bg-discord-hover hover:text-white transition-colors {{if eq .ActivePage "convocations"}}sidebar-link-active{{end}}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 6l3 1m0 0l-3 9a5.002 5.002 0 006.001 0M6 7l3 9M6 7l6-2m6 2l3-1m-3 1l-3 9a5.002 5.002 0 006.001 0M18 7l3 9m-3-9l-6-2m0-2v2m0 16V5m0 16H9m3 0h3"/></svg>
|
||||
Convocations
|
||||
</a>
|
||||
<a href="/transcripts" class="sidebar-link flex items-center gap-3 px-3 py-2 rounded text-gray-300 hover:bg-discord-hover hover:text-white transition-colors {{if eq .ActivePage "transcripts"}}sidebar-link-active{{end}}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
|
||||
Transcripts
|
||||
</a>
|
||||
<a href="/audit" class="sidebar-link flex items-center gap-3 px-3 py-2 rounded text-gray-300 hover:bg-discord-hover hover:text-white transition-colors {{if eq .ActivePage "audit"}}sidebar-link-active{{end}}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/></svg>
|
||||
Audit Log
|
||||
</a>
|
||||
{{if .Admin}}{{if .Admin.IsSuperadmin}}
|
||||
<a href="/sessions" class="sidebar-link flex items-center gap-3 px-3 py-2 rounded text-gray-300 hover:bg-discord-hover hover:text-white transition-colors {{if eq .ActivePage "sessions"}}sidebar-link-active{{end}}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/></svg>
|
||||
Sessions
|
||||
</a>
|
||||
{{end}}{{end}}
|
||||
</nav>
|
||||
|
||||
<!-- Admin + logout -->
|
||||
<div class="p-3 border-t border-discord-border">
|
||||
{{if .Admin}}
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
{{if .Admin.DiscordAvatar}}
|
||||
<img src="https://cdn.discordapp.com/avatars/{{.Admin.DiscordID}}/{{.Admin.DiscordAvatar}}.png?size=32"
|
||||
class="w-8 h-8 rounded-full" alt="" onerror="this.style.display='none'">
|
||||
{{end}}
|
||||
<div class="min-w-0">
|
||||
<p class="text-sm font-medium text-white truncate">{{.Admin.DiscordUsername}}</p>
|
||||
{{if .Admin.IsSuperadmin}}<p class="text-xs text-discord-accent">Superadmin</p>{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<form method="POST" action="/auth/logout">
|
||||
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
||||
<button type="submit" class="w-full text-left flex items-center gap-2 px-2 py-1.5 rounded text-gray-400 hover:text-red-400 hover:bg-discord-hover transition-colors text-sm">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/></svg>
|
||||
Déconnexion
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main content -->
|
||||
<div class="flex-1 flex flex-col min-w-0 h-screen overflow-hidden">
|
||||
<!-- Header -->
|
||||
<header class="h-14 flex items-center justify-between px-6 border-b border-discord-border bg-discord-card flex-shrink-0">
|
||||
<h2 class="font-semibold text-white">{{block "header" .}}Dashboard{{end}}</h2>
|
||||
<div class="flex items-center gap-3">
|
||||
{{if .BotOnline}}
|
||||
<span class="flex items-center gap-1.5 text-sm text-green-400">
|
||||
<span class="w-2 h-2 bg-green-400 rounded-full"></span>Bot en ligne
|
||||
</span>
|
||||
{{else}}
|
||||
<span class="flex items-center gap-1.5 text-sm text-red-400">
|
||||
<span class="w-2 h-2 bg-red-400 rounded-full"></span>Bot hors ligne
|
||||
</span>
|
||||
{{end}}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Page content -->
|
||||
<main class="flex-1 overflow-y-auto p-6">
|
||||
{{if .Flash}}
|
||||
<div id="flash" class="mb-4 px-4 py-3 rounded bg-green-900/50 border border-green-700 text-green-300 text-sm toast-enter">
|
||||
{{.Flash}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .Error}}
|
||||
<div id="error-msg" class="mb-4 px-4 py-3 rounded bg-red-900/50 border border-red-700 text-red-300 text-sm toast-enter">
|
||||
{{.Error}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{block "content" .}}{{end}}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Toast container for HTMX OOB swaps -->
|
||||
<div id="toast-container" class="fixed bottom-4 right-4 space-y-2 z-50"></div>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
@@ -0,0 +1,20 @@
|
||||
{{define "content"}}
|
||||
<div class="text-center">
|
||||
<h2 class="text-xl font-semibold text-white mb-2">Connexion</h2>
|
||||
<p class="text-gray-400 text-sm mb-8">Connecte-toi avec ton compte Discord pour accéder au panel.</p>
|
||||
|
||||
{{if .Error}}
|
||||
<div class="mb-4 px-4 py-3 rounded bg-red-900/50 border border-red-700 text-red-300 text-sm text-left">
|
||||
{{.Error}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<a href="{{.OAuthURL}}"
|
||||
class="flex items-center justify-center gap-3 w-full px-4 py-3 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-white font-medium transition-colors">
|
||||
<svg class="w-5 h-5" viewBox="0 0 127.14 96.36" fill="currentColor">
|
||||
<path d="M107.7,8.07A105.15,105.15,0,0,0,81.47,0a72.06,72.06,0,0,0-3.36,6.83A97.68,97.68,0,0,0,49,6.83,72.37,72.37,0,0,0,45.64,0,105.89,105.89,0,0,0,19.39,8.09C2.79,32.65-1.71,56.6.54,80.21h0A105.73,105.73,0,0,0,32.71,96.36,77.7,77.7,0,0,0,39.6,85.25a68.42,68.42,0,0,1-10.85-5.18c.91-.66,1.8-1.34,2.66-2a75.57,75.57,0,0,0,64.32,0c.87.71,1.76,1.39,2.66,2a68.68,68.68,0,0,1-10.87,5.19,77,77,0,0,0,6.89,11.1A105.25,105.25,0,0,0,126.6,80.22h0C129.24,52.84,122.09,29.11,107.7,8.07ZM42.45,65.69C36.18,65.69,31,60,31,53s5-12.74,11.43-12.74S54,46,53.89,53,48.84,65.69,42.45,65.69Zm42.24,0C78.41,65.69,73.25,60,73.25,53s5-12.74,11.44-12.74S96.23,46,96.12,53,91.08,65.69,84.69,65.69Z"/>
|
||||
</svg>
|
||||
Se connecter avec Discord
|
||||
</a>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,431 @@
|
||||
{{define "title"}}{{if .IsNew}}Nouveau panel{{else}}Éditer — {{.Panel.Name}}{{end}}{{end}}
|
||||
{{define "header"}}{{if .IsNew}}Créer un panel{{else}}Éditer {{.Panel.Name}}{{end}}{{end}}
|
||||
{{define "content"}}
|
||||
<form id="panelForm" method="POST" action="{{if .IsNew}}/panels/new{{else}}/panels/{{.Panel.ID}}/edit{{end}}">
|
||||
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
||||
|
||||
{{if and .IsNew .Guilds}}
|
||||
<div class="bg-discord-card rounded-lg border border-discord-border p-4 mb-4 flex items-center gap-4">
|
||||
<label class="text-sm text-gray-300 flex-shrink-0">Serveur Discord :</label>
|
||||
<select id="guildSelector" class="px-3 py-2 bg-gray-800 border border-gray-600 rounded-lg text-white text-sm focus:outline-none focus:border-indigo-500"
|
||||
onchange="location.href='/panels/new?guild_id='+this.value">
|
||||
{{range .Guilds}}
|
||||
<option value="{{.ID}}" {{if eq .ID $.Panel.GuildID}}selected{{end}}>{{.Name}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
<p class="text-xs text-gray-500">Les rôles et channels chargés correspondent au serveur sélectionné.</p>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<input type="hidden" name="guild_id" value="{{.Panel.GuildID}}">
|
||||
|
||||
<div class="grid grid-cols-1 xl:grid-cols-3 gap-6">
|
||||
<!-- Left: form -->
|
||||
<div class="xl:col-span-2 space-y-6">
|
||||
|
||||
<!-- Section 1: Infos panel -->
|
||||
<div class="bg-discord-card rounded-lg border border-discord-border p-6">
|
||||
<h2 class="font-semibold text-white mb-4">Informations du panel</h2>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div class="sm:col-span-2">
|
||||
<label class="block text-sm text-gray-300 mb-1">Nom interne <span class="text-red-400">*</span></label>
|
||||
<input type="text" name="name" value="{{.Panel.Name}}" required {{if not .IsNew}}readonly{{end}}
|
||||
class="w-full px-3 py-2 bg-gray-800 border {{if index .Errors "name"}}border-red-600{{else}}border-gray-600{{end}} rounded-lg text-white focus:outline-none focus:border-indigo-500 {{if not .IsNew}}opacity-60 cursor-not-allowed{{end}}">
|
||||
{{if index .Errors "name"}}<p class="text-red-400 text-xs mt-1">{{index .Errors "name"}}</p>{{end}}
|
||||
</div>
|
||||
<div class="sm:col-span-2">
|
||||
<label class="block text-sm text-gray-300 mb-1">Titre embed <span class="text-red-400">*</span></label>
|
||||
<input type="text" name="embed_title" value="{{.Panel.EmbedTitle}}" required
|
||||
hx-post="/panels/preview" hx-trigger="input changed delay:300ms" hx-include="#panelForm" hx-target="#preview"
|
||||
class="w-full px-3 py-2 bg-gray-800 border {{if index .Errors "embed_title"}}border-red-600{{else}}border-gray-600{{end}} rounded-lg text-white focus:outline-none focus:border-indigo-500">
|
||||
{{if index .Errors "embed_title"}}<p class="text-red-400 text-xs mt-1">{{index .Errors "embed_title"}}</p>{{end}}
|
||||
</div>
|
||||
<div class="sm:col-span-2">
|
||||
<label class="block text-sm text-gray-300 mb-1">Description embed</label>
|
||||
<textarea name="embed_description" rows="3"
|
||||
hx-post="/panels/preview" hx-trigger="input changed delay:300ms" hx-include="#panelForm" hx-target="#preview"
|
||||
class="w-full px-3 py-2 bg-gray-800 border border-gray-600 rounded-lg text-white focus:outline-none focus:border-indigo-500 resize-none">{{.Panel.EmbedDescription.String}}</textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-300 mb-1">Couleur embed</label>
|
||||
<div class="flex items-center gap-3">
|
||||
<input type="color" name="embed_color" value="{{.Panel.EmbedColor}}" id="embedColor"
|
||||
hx-post="/panels/preview" hx-trigger="input changed delay:200ms" hx-include="#panelForm" hx-target="#preview"
|
||||
class="h-9 w-16 rounded cursor-pointer bg-gray-800 border border-gray-600">
|
||||
<span id="embedColorText" class="text-sm text-gray-400">{{.Panel.EmbedColor}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sm:col-span-2">
|
||||
<label class="block text-sm text-gray-300 mb-1">Image URL (grande image en bas de l'embed)</label>
|
||||
<input type="url" name="embed_image" value="{{.Panel.EmbedImage}}"
|
||||
class="w-full px-3 py-2 bg-gray-800 border border-gray-600 rounded-lg text-white focus:outline-none focus:border-indigo-500"
|
||||
placeholder="https://...">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-300 mb-1">Channel Discord</label>
|
||||
{{if .TextChans}}
|
||||
<select name="channel_id" class="w-full px-3 py-2 bg-gray-800 border border-gray-600 rounded-lg text-white focus:outline-none focus:border-indigo-500">
|
||||
<option value="">— Sélectionner —</option>
|
||||
{{range .TextChans}}
|
||||
<option value="{{.ID}}" {{if eq .ID $.Panel.ChannelID.String}}selected{{end}}>#{{.Name}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
{{else}}
|
||||
<input type="text" name="channel_id" value="{{.Panel.ChannelID.String}}"
|
||||
class="w-full px-3 py-2 bg-gray-800 border border-gray-600 rounded-lg text-white focus:outline-none focus:border-indigo-500"
|
||||
placeholder="ID du channel cible">
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 2: Types de tickets -->
|
||||
<div class="bg-discord-card rounded-lg border border-discord-border p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="font-semibold text-white">Types de tickets</h2>
|
||||
<button type="button" onclick="addType()"
|
||||
class="px-3 py-1.5 text-sm bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg transition-colors">
|
||||
+ Ajouter un type
|
||||
</button>
|
||||
</div>
|
||||
<div id="typesList" class="space-y-4">
|
||||
{{range $i, $t := .Panel.Types}}
|
||||
{{template "type_block" dict "Type" $t "Index" $i "CSRFToken" $.CSRFToken "Roles" $.Roles "TextChans" $.TextChans "Categories" $.Categories}}
|
||||
{{end}}
|
||||
</div>
|
||||
{{if eq (len .Panel.Types) 0}}
|
||||
<p id="noTypesMsg" class="text-gray-500 text-sm text-center py-4">Aucun type. Cliquez sur "+ Ajouter un type".</p>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Right: preview + actions -->
|
||||
<div class="space-y-6">
|
||||
<div class="bg-discord-card rounded-lg border border-discord-border p-5">
|
||||
<h3 class="font-semibold text-white mb-3">Aperçu</h3>
|
||||
<div id="preview">
|
||||
<div class="rounded-lg overflow-hidden border-l-4 bg-[#2b2d31] p-4" style="border-left-color:{{.Panel.EmbedColor}}">
|
||||
<p class="font-semibold text-white mb-1">{{if .Panel.EmbedTitle}}{{.Panel.EmbedTitle}}{{else}}Titre du panel{{end}}</p>
|
||||
{{if .Panel.EmbedDescription.Valid}}
|
||||
<p class="text-gray-300 text-sm">{{.Panel.EmbedDescription.String}}</p>
|
||||
{{end}}
|
||||
{{if .Panel.Types}}
|
||||
<div class="flex flex-wrap gap-2 mt-3">
|
||||
{{range .Panel.Types}}
|
||||
<span class="{{buttonClass .ButtonColor}} text-white text-sm px-3 py-1.5 rounded">{{.ButtonEmoji}} {{.ButtonLabel}}</span>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-discord-card rounded-lg border border-discord-border p-5 space-y-3">
|
||||
<button type="submit" name="action" value="save"
|
||||
class="w-full px-4 py-2.5 bg-indigo-600 hover:bg-indigo-500 text-white font-medium rounded-lg transition-colors">
|
||||
Sauvegarder
|
||||
</button>
|
||||
<button type="submit" name="action" value="save_send"
|
||||
class="w-full px-4 py-2.5 bg-green-700 hover:bg-green-600 text-white font-medium rounded-lg transition-colors">
|
||||
Sauvegarder et envoyer sur Discord
|
||||
</button>
|
||||
<a href="/panels" class="block text-center px-4 py-2.5 text-gray-400 hover:text-white border border-gray-700 hover:border-gray-500 rounded-lg transition-colors text-sm">
|
||||
Annuler
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Emoji picker -->
|
||||
<div id="emojiPicker" class="hidden fixed z-50 bg-gray-900 border border-gray-700 rounded-xl shadow-2xl w-72">
|
||||
<div class="flex gap-1 p-2 border-b border-gray-700 overflow-x-auto">
|
||||
<button type="button" onclick="showEmojiCat('smileys')" class="emoji-cat-btn px-2 py-1 text-sm rounded hover:bg-gray-700" data-cat="smileys">😀</button>
|
||||
<button type="button" onclick="showEmojiCat('gestures')" class="emoji-cat-btn px-2 py-1 text-sm rounded hover:bg-gray-700" data-cat="gestures">👍</button>
|
||||
<button type="button" onclick="showEmojiCat('activities')" class="emoji-cat-btn px-2 py-1 text-sm rounded hover:bg-gray-700" data-cat="activities">🎮</button>
|
||||
<button type="button" onclick="showEmojiCat('objects')" class="emoji-cat-btn px-2 py-1 text-sm rounded hover:bg-gray-700" data-cat="objects">📝</button>
|
||||
<button type="button" onclick="showEmojiCat('symbols')" class="emoji-cat-btn px-2 py-1 text-sm rounded hover:bg-gray-700" data-cat="symbols">✅</button>
|
||||
</div>
|
||||
<div id="emojiGrid" class="p-2 grid grid-cols-8 gap-0.5 max-h-48 overflow-y-auto"></div>
|
||||
</div>
|
||||
|
||||
<!-- Type block template for JS -->
|
||||
<template id="typeTpl">
|
||||
<div class="type-block border border-gray-700 rounded-lg p-4 space-y-3 cursor-move" data-index="__IDX__">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<span class="text-sm font-medium text-gray-300 flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8h16M4 16h16"/></svg>
|
||||
Type <span class="type-num"></span>
|
||||
</span>
|
||||
<button type="button" onclick="removeType(this)" class="text-red-400 hover:text-red-300 text-sm">Supprimer</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div><label class="block text-xs text-gray-400 mb-1">Nom interne</label>
|
||||
<input type="text" name="types[__IDX__][name]" required class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500" placeholder="ex: support"></div>
|
||||
<div><label class="block text-xs text-gray-400 mb-1">Label bouton</label>
|
||||
<input type="text" name="types[__IDX__][button_label]"
|
||||
hx-post="/panels/preview" hx-trigger="input changed delay:300ms" hx-include="#panelForm" hx-target="#preview"
|
||||
class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500"></div>
|
||||
<div><label class="block text-xs text-gray-400 mb-1">Couleur bouton</label>
|
||||
<select name="types[__IDX__][button_color]"
|
||||
hx-post="/panels/preview" hx-trigger="change" hx-include="#panelForm" hx-target="#preview"
|
||||
class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500">
|
||||
<option value="primary">🔵 Bleu (primary)</option>
|
||||
<option value="secondary">⚪ Gris (secondary)</option>
|
||||
<option value="success">🟢 Vert (success)</option>
|
||||
<option value="danger">🔴 Rouge (danger)</option>
|
||||
</select></div>
|
||||
<div><label class="block text-xs text-gray-400 mb-1">Emoji bouton</label>
|
||||
<div class="flex gap-1">
|
||||
<input type="text" name="types[__IDX__][button_emoji]" readonly
|
||||
hx-post="/panels/preview" hx-trigger="input changed delay:200ms" hx-include="#panelForm" hx-target="#preview"
|
||||
class="flex-1 px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none cursor-pointer" placeholder="—" onclick="openEmojiPicker(this)">
|
||||
<button type="button" onclick="openEmojiPicker(this.previousElementSibling)" class="px-2 py-1.5 bg-gray-700 hover:bg-gray-600 border border-gray-600 rounded text-sm">😀</button>
|
||||
</div></div>
|
||||
<div><label class="block text-xs text-gray-400 mb-1">Titre embed ticket</label>
|
||||
<input type="text" name="types[__IDX__][embed_title]" class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500"></div>
|
||||
<div><label class="block text-xs text-gray-400 mb-1">Couleur embed</label>
|
||||
<input type="color" name="types[__IDX__][embed_color]" value="#5865f2" class="h-9 w-full rounded cursor-pointer bg-gray-800 border border-gray-600"></div>
|
||||
<div class="col-span-2"><label class="block text-xs text-gray-400 mb-1">Texte embed bienvenue</label>
|
||||
<textarea name="types[__IDX__][embed_text]" rows="2" class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500 resize-none"></textarea></div>
|
||||
<div class="col-span-2"><label class="block text-xs text-gray-400 mb-1">Image URL (optionnel)</label>
|
||||
<input type="url" name="types[__IDX__][image_url]" class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500" placeholder="https://..."></div>
|
||||
__ROLES_HTML__
|
||||
__CATEGORIES_HTML__
|
||||
__TEXTCHANS_HTML__
|
||||
<div><label class="block text-xs text-gray-400 mb-1">Règle de fermeture</label>
|
||||
<select name="types[__IDX__][close_rule]" class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500">
|
||||
<option value="staff_only">Staff uniquement</option>
|
||||
<option value="author_if_unclaimed">Auteur si non claimé</option>
|
||||
</select></div>
|
||||
<div class="col-span-2"><label class="block text-xs text-gray-400 mb-1">Reup claim (minutes, 0 = global)</label>
|
||||
<input type="number" name="types[__IDX__][claim_reup_minutes]" value="0" min="0" max="1440"
|
||||
class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500"></div>
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="hidden" name="types[__IDX__][claim_mode]" value="0">
|
||||
<input type="checkbox" name="types[__IDX__][claim_mode]" value="1" class="rounded" id="cm__IDX__">
|
||||
<label for="cm__IDX__" class="text-xs text-gray-400">Système de claim activé</label></div>
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="hidden" name="types[__IDX__][modal_enabled]" value="1">
|
||||
<input type="checkbox" name="types[__IDX__][modal_enabled]" value="1" checked class="rounded" id="me__IDX__">
|
||||
<label for="me__IDX__" class="text-xs text-gray-400">Modal activé</label></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script>
|
||||
<script>
|
||||
let typeCount = {{len .Panel.Types}};
|
||||
|
||||
// Emoji data
|
||||
const emojiData = {
|
||||
smileys: ['😀','😃','😄','😁','😆','😅','😂','🤣','😊','😇','🙂','😉','😌','😍','🥰','😘','😗','😙','😚','😋','😛','😜','🤪','😝','🤑','🤗','🤭','🤫','🤔','🤐','🤨','😐','😑','😶','😏','😒','🙄','😬','🤥','😌','😔','😪','🤤','😴','😷','🤒','🤕','🤢','🤧','🥵','🥶','🥴','😵','🤯','🤠','🥳','😎','🤓','🧐','😕','😟','🙁','☹️','😮','😯','😲','😳','🥺','😦','😧','😨','😰','😥','😢','😭','😱','😖','😣','😞','😓','😩','😫','🥱','😤','😡','😠','🤬','😈','👿','💀','☠️','💩','🤡','👹','👺','👻','👽','👾','🤖'],
|
||||
gestures: ['👋','🤚','🖐','✋','🖖','👌','🤌','🤏','✌️','🤞','🤟','🤘','🤙','👈','👉','👆','🖕','👇','☝️','👍','👎','✊','👊','🤛','🤜','👏','🙌','👐','🤲','🤝','🙏','✍️','💅','🤳','💪','🦾','🦿','🦵','🦶','👂','🦻','👃','🫀','🫁','🧠','🦷','🦴','👀','👁','👅','👄'],
|
||||
activities: ['⚽','🏀','🏈','⚾','🥎','🎾','🏐','🏉','🥏','🎱','🪀','🏓','🏸','🏒','🥍','🏏','🪃','🥅','⛳','🪁','🏹','🎣','🤿','🥊','🥋','🎽','🛹','🛷','⛸','🥌','🎿','⛷','🏂','🏋','🤼','🤸','⛹','🤺','🤾','🏌','🏇','🧘','🏄','🏊','🤽','🚣','🧗','🚵','🚴','🎖','🏆','🥇','🥈','🥉','🏅','🎪','🤹','🎭','🎨','🎬','🎤','🎧','🎼','🎵','🎶','🎷','🪗','🎸','🎹','🎺','🎻','🥁','🪘','🎙','🎚','🎛','📻','🎮','🕹','🎲','🎯','🎳','🎰','🎴','🀄','🎭','🎠','🎡','🎢','🎪'],
|
||||
objects: ['📱','💻','🖥','🖨','⌨️','🖱','🖲','💽','💾','💿','📀','📷','📸','📹','🎥','📽','🎞','📞','☎️','📟','📠','📺','📻','🧭','⏱','⏲','⏰','🕰','⌚','⏳','📡','🔋','🔌','💡','🔦','🕯','🗑','🛢','💰','💴','💵','💶','💷','💸','💳','🪙','💹','📈','📉','📊','📋','📌','📍','✂️','🗃','🗂','🗄','🗑','🔧','🔨','⚒','🛠','⛏','🔩','🪛','🔑','🗝','🔐','🔏','🔒','🔓','🚪','🪑','🛋','🚽','🪠','🚿','🛁','🛒','🚬','⚰️','🪦','⚱️','🧿','💎','🔮','🧸','🪆','🖼','🪅','🎊','🎉','🎁','🎀','🎗','🎟','🎫','🎖','📝','📓','📔','📒','📕','📗','📘','📙','📚','📖','🔖','🏷','💉','🩸','💊','🩹','🩺','🔬','🔭','🩻','🧬','🦠','🧫','🧪','🌡','🧹','🧺','🧻','🪣','🧼','🫧','🪥','🧽','🧯','🛒'],
|
||||
symbols: ['✅','❌','❎','🔰','⛔','🚫','📵','🔕','🚷','🚯','🚳','🚱','🔞','📵','🔇','🔈','🔉','🔊','📢','📣','🔔','🔕','🔋','🪫','🔌','💡','🔦','🕯','🪔','🧯','💰','💱','💲','♾','🔁','🔂','▶️','⏩','⏭','⏯','◀️','⏪','⏮','🔀','🔃','🎵','🎶','➕','➖','✖️','➗','🟰','♾','💲','💱','🏧','🔱','⚜️','🔰','♻️','✅','🆗','✔️','❎','🆘','❓','❔','❕','❗','⁉️','‼️','🔅','🔆','🔱','⚜️','💠','🌀','⭕','✖️','❌','✖','➰','➿','〽️','✳️','✴️','❇️','💯','☑️','🔘','🔲','🔳','▪️','▫️','◾','◽','◼️','◻️','🟥','🟧','🟨','🟩','🟦','🟪','⬛','⬜','🟫','🔶','🔷','🔸','🔹','🔺','🔻','💠','🔘','🔳','🔲']
|
||||
};
|
||||
|
||||
let currentEmojiInput = null;
|
||||
let currentCat = 'smileys';
|
||||
|
||||
function openEmojiPicker(input) {
|
||||
currentEmojiInput = input;
|
||||
const picker = document.getElementById('emojiPicker');
|
||||
const rect = input.getBoundingClientRect();
|
||||
picker.style.top = (window.scrollY + rect.bottom + 4) + 'px';
|
||||
picker.style.left = rect.left + 'px';
|
||||
picker.classList.remove('hidden');
|
||||
showEmojiCat(currentCat);
|
||||
}
|
||||
|
||||
function showEmojiCat(cat) {
|
||||
currentCat = cat;
|
||||
const grid = document.getElementById('emojiGrid');
|
||||
grid.innerHTML = emojiData[cat].map(e =>
|
||||
`<button type="button" onclick="pickEmoji('${e}')" class="text-xl p-1 hover:bg-gray-700 rounded transition-colors" title="${e}">${e}</button>`
|
||||
).join('');
|
||||
}
|
||||
|
||||
function pickEmoji(e) {
|
||||
if (currentEmojiInput) {
|
||||
currentEmojiInput.value = e;
|
||||
currentEmojiInput.dispatchEvent(new Event('input', {bubbles: true}));
|
||||
}
|
||||
document.getElementById('emojiPicker').classList.add('hidden');
|
||||
}
|
||||
|
||||
document.addEventListener('click', function(ev) {
|
||||
const picker = document.getElementById('emojiPicker');
|
||||
if (!picker.classList.contains('hidden') && !picker.contains(ev.target) &&
|
||||
!ev.target.closest('[onclick*="openEmojiPicker"]') && !ev.target.closest('[onclick*="EmojiPicker"]')) {
|
||||
picker.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// Sync color picker
|
||||
document.getElementById('embedColor').addEventListener('input', function() {
|
||||
document.getElementById('embedColorText').textContent = this.value;
|
||||
});
|
||||
|
||||
// Roles HTML for JS-added type blocks
|
||||
const rolesHtml = `{{range .Roles}}<option value="{{.ID}}">{{.Name}}</option>{{end}}`;
|
||||
const categoriesHtml = `{{range .Categories}}<option value="{{.ID}}">📁 {{.Name}}</option>{{end}}`;
|
||||
const textChansHtml = `{{range .TextChans}}<option value="{{.ID}}"># {{.Name}}</option>{{end}}`;
|
||||
|
||||
function makeSelect(name, label, options, value = '') {
|
||||
if (!options) return `<div><label class="block text-xs text-gray-400 mb-1">${label}</label><input type="text" name="${name}" value="${value}" class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500"></div>`;
|
||||
return `<div><label class="block text-xs text-gray-400 mb-1">${label}</label><select name="${name}" class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500"><option value="">— Sélectionner —</option>${options}</select></div>`;
|
||||
}
|
||||
|
||||
// SortableJS
|
||||
new Sortable(document.getElementById('typesList'), {
|
||||
animation: 150, handle: '.type-block',
|
||||
onEnd: function() {
|
||||
renumberTypes();
|
||||
{{if not .IsNew}}
|
||||
const ids = [...document.querySelectorAll('.type-block')].map(el => el.dataset.typeId).filter(Boolean);
|
||||
if (ids.length) {
|
||||
fetch('/panels/{{.Panel.ID}}/types/reorder', {
|
||||
method: 'POST',
|
||||
headers: {'X-CSRF-Token': '{{.CSRFToken}}', 'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: ids.map(id => 'ids[]=' + id).join('&')
|
||||
});
|
||||
}
|
||||
{{end}}
|
||||
}
|
||||
});
|
||||
|
||||
function addType() {
|
||||
document.getElementById('noTypesMsg')?.remove();
|
||||
let html = document.getElementById('typeTpl').innerHTML.replaceAll('__IDX__', typeCount);
|
||||
html = html.replace('__ROLES_HTML__', makeSelect(`types[${typeCount}][staff_role_id]`, 'Staff Role', rolesHtml));
|
||||
html = html.replace('__CATEGORIES_HTML__', makeSelect(`types[${typeCount}][category_id]`, 'Catégorie', categoriesHtml));
|
||||
html = html.replace('__TEXTCHANS_HTML__',
|
||||
makeSelect(`types[${typeCount}][claim_channel_id]`, 'Claim Channel', textChansHtml) +
|
||||
makeSelect(`types[${typeCount}][log_channel_id]`, 'Log Channel', textChansHtml));
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = html;
|
||||
document.getElementById('typesList').appendChild(div.firstElementChild);
|
||||
typeCount++;
|
||||
renumberTypes();
|
||||
htmx.process(document.getElementById('typesList'));
|
||||
}
|
||||
|
||||
function removeType(btn) {
|
||||
btn.closest('.type-block').remove();
|
||||
renumberTypes();
|
||||
}
|
||||
|
||||
function renumberTypes() {
|
||||
document.querySelectorAll('.type-num').forEach((el, i) => el.textContent = i + 1);
|
||||
}
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
{{define "type_block"}}
|
||||
<div class="type-block border border-gray-700 rounded-lg p-4 space-y-3 cursor-move" data-index="{{.Index}}" data-type-id="{{.Type.ID}}">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<span class="text-sm font-medium text-gray-300 flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8h16M4 16h16"/></svg>
|
||||
Type <span class="type-num">{{add .Index 1}}</span>
|
||||
</span>
|
||||
<button type="button" onclick="removeType(this)" class="text-red-400 hover:text-red-300 text-sm">Supprimer</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div><label class="block text-xs text-gray-400 mb-1">Nom interne</label>
|
||||
<input type="text" name="types[{{.Index}}][name]" value="{{.Type.Name}}" required class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500"></div>
|
||||
<div><label class="block text-xs text-gray-400 mb-1">Label bouton</label>
|
||||
<input type="text" name="types[{{.Index}}][button_label]" value="{{.Type.ButtonLabel}}"
|
||||
hx-post="/panels/preview" hx-trigger="input changed delay:300ms" hx-include="#panelForm" hx-target="#preview"
|
||||
class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500"></div>
|
||||
<div><label class="block text-xs text-gray-400 mb-1">Couleur bouton</label>
|
||||
<select name="types[{{.Index}}][button_color]"
|
||||
hx-post="/panels/preview" hx-trigger="change" hx-include="#panelForm" hx-target="#preview"
|
||||
class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500">
|
||||
<option value="primary" {{if eq .Type.ButtonColor "primary"}}selected{{end}}>🔵 Bleu (primary)</option>
|
||||
<option value="secondary" {{if eq .Type.ButtonColor "secondary"}}selected{{end}}>⚪ Gris (secondary)</option>
|
||||
<option value="success" {{if eq .Type.ButtonColor "success"}}selected{{end}}>🟢 Vert (success)</option>
|
||||
<option value="danger" {{if eq .Type.ButtonColor "danger"}}selected{{end}}>🔴 Rouge (danger)</option>
|
||||
</select></div>
|
||||
<div><label class="block text-xs text-gray-400 mb-1">Emoji bouton</label>
|
||||
<div class="flex gap-1">
|
||||
<input type="text" name="types[{{.Index}}][button_emoji]" value="{{.Type.ButtonEmoji}}" readonly
|
||||
hx-post="/panels/preview" hx-trigger="input changed delay:200ms" hx-include="#panelForm" hx-target="#preview"
|
||||
class="flex-1 px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm cursor-pointer" placeholder="—" onclick="openEmojiPicker(this)">
|
||||
<button type="button" onclick="openEmojiPicker(this.previousElementSibling)" class="px-2 py-1.5 bg-gray-700 hover:bg-gray-600 border border-gray-600 rounded text-sm">😀</button>
|
||||
</div></div>
|
||||
<div><label class="block text-xs text-gray-400 mb-1">Titre embed ticket</label>
|
||||
<input type="text" name="types[{{.Index}}][embed_title]" value="{{.Type.EmbedTitle}}" class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500"></div>
|
||||
<div><label class="block text-xs text-gray-400 mb-1">Couleur embed</label>
|
||||
<input type="color" name="types[{{.Index}}][embed_color]" value="{{.Type.EmbedColor}}" class="h-9 w-full rounded cursor-pointer bg-gray-800 border border-gray-600"></div>
|
||||
<div class="col-span-2"><label class="block text-xs text-gray-400 mb-1">Texte embed bienvenue</label>
|
||||
<textarea name="types[{{.Index}}][embed_text]" rows="2" class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500 resize-none">{{.Type.EmbedText}}</textarea></div>
|
||||
<div class="col-span-2"><label class="block text-xs text-gray-400 mb-1">Image URL (optionnel)</label>
|
||||
<input type="url" name="types[{{.Index}}][image_url]" value="{{.Type.ImageURL}}" class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500" placeholder="https://..."></div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-400 mb-1">Staff Role</label>
|
||||
{{if .Roles}}
|
||||
<select name="types[{{.Index}}][staff_role_id]" class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500">
|
||||
<option value="">— Sélectionner —</option>
|
||||
{{range .Roles}}<option value="{{.ID}}" {{if eq .ID $.Type.StaffRoleID}}selected{{end}}>{{.Name}}</option>{{end}}
|
||||
</select>
|
||||
{{else}}
|
||||
<input type="text" name="types[{{.Index}}][staff_role_id]" value="{{.Type.StaffRoleID}}" class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500">
|
||||
{{end}}
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-400 mb-1">Catégorie</label>
|
||||
{{if .Categories}}
|
||||
<select name="types[{{.Index}}][category_id]" class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500">
|
||||
<option value="">— Sélectionner —</option>
|
||||
{{range .Categories}}<option value="{{.ID}}" {{if eq .ID $.Type.CategoryID}}selected{{end}}>📁 {{.Name}}</option>{{end}}
|
||||
</select>
|
||||
{{else}}
|
||||
<input type="text" name="types[{{.Index}}][category_id]" value="{{.Type.CategoryID}}" class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500">
|
||||
{{end}}
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-400 mb-1">Claim Channel</label>
|
||||
{{if .TextChans}}
|
||||
<select name="types[{{.Index}}][claim_channel_id]" class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500">
|
||||
<option value="">— Sélectionner —</option>
|
||||
{{range .TextChans}}<option value="{{.ID}}" {{if eq .ID $.Type.ClaimChannelID}}selected{{end}}># {{.Name}}</option>{{end}}
|
||||
</select>
|
||||
{{else}}
|
||||
<input type="text" name="types[{{.Index}}][claim_channel_id]" value="{{.Type.ClaimChannelID}}" class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500">
|
||||
{{end}}
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-400 mb-1">Log Channel</label>
|
||||
{{if .TextChans}}
|
||||
<select name="types[{{.Index}}][log_channel_id]" class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500">
|
||||
<option value="">— Sélectionner —</option>
|
||||
{{range .TextChans}}<option value="{{.ID}}" {{if eq .ID $.Type.LogChannelID}}selected{{end}}># {{.Name}}</option>{{end}}
|
||||
</select>
|
||||
{{else}}
|
||||
<input type="text" name="types[{{.Index}}][log_channel_id]" value="{{.Type.LogChannelID}}" class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500">
|
||||
{{end}}
|
||||
</div>
|
||||
<div><label class="block text-xs text-gray-400 mb-1">Max tickets / user</label>
|
||||
<input type="number" name="types[{{.Index}}][max_per_user]" value="{{.Type.MaxPerUser}}" min="1" class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500"></div>
|
||||
<div><label class="block text-xs text-gray-400 mb-1">Règle de close</label>
|
||||
<select name="types[{{.Index}}][close_rule]" class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500">
|
||||
<option value="staff_only" {{if eq .Type.CloseRule "staff_only"}}selected{{end}}>Staff uniquement</option>
|
||||
<option value="author_if_unclaimed" {{if eq .Type.CloseRule "author_if_unclaimed"}}selected{{end}}>Auteur si non claimé</option>
|
||||
</select></div>
|
||||
<div class="flex items-center gap-2 pt-1">
|
||||
<input type="hidden" name="types[{{.Index}}][claim_mode]" value="0">
|
||||
<input type="checkbox" name="types[{{.Index}}][claim_mode]" value="1" {{if .Type.ClaimMode}}checked{{end}} class="rounded" id="cm{{.Index}}">
|
||||
<label for="cm{{.Index}}" class="text-xs text-gray-400">Système de claim activé</label></div>
|
||||
<div class="col-span-2"><label class="block text-xs text-gray-400 mb-1">Reup claim (minutes, 0 = global)</label>
|
||||
<input type="number" name="types[{{.Index}}][claim_reup_minutes]" value="{{.Type.ClaimReupMinutes}}" min="0" max="1440"
|
||||
class="w-full px-2 py-1.5 bg-gray-800 border border-gray-600 rounded text-white text-sm focus:outline-none focus:border-indigo-500"></div>
|
||||
<div class="flex items-center gap-2 pt-1">
|
||||
<input type="hidden" name="types[{{.Index}}][modal_enabled]" value="0">
|
||||
<input type="checkbox" name="types[{{.Index}}][modal_enabled]" value="1" {{if .Type.ModalEnabled}}checked{{end}} class="rounded" id="me{{.Index}}">
|
||||
<label for="me{{.Index}}" class="text-xs text-gray-400">Modal activé</label></div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,91 @@
|
||||
{{define "title"}}Panels{{end}}
|
||||
{{define "header"}}Panels{{end}}
|
||||
{{define "content"}}
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<p class="text-gray-400 text-sm">{{len .Panels}} panel{{if gt (len .Panels) 1}}s{{end}} configuré{{if gt (len .Panels) 1}}s{{end}}</p>
|
||||
<a href="/panels/new" class="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white text-sm font-medium rounded-lg transition-colors">
|
||||
+ Créer un panel
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{{if .Panels}}
|
||||
<div class="grid gap-4">
|
||||
{{range .Panels}}
|
||||
<div class="bg-discord-card rounded-lg border border-discord-border p-5 flex items-center gap-4">
|
||||
<!-- Color swatch -->
|
||||
<div class="w-1 self-stretch rounded-full flex-shrink-0" style="background-color:{{.EmbedColor}}"></div>
|
||||
<!-- Info -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<span class="font-semibold text-white">{{.Name}}</span>
|
||||
<span class="text-xs text-gray-500 bg-gray-800 px-2 py-0.5 rounded">{{len .Types}} type{{if gt (len .Types) 1}}s{{end}}</span>
|
||||
{{if .MessageID.Valid}}<span class="text-xs text-green-400 bg-green-900/30 px-2 py-0.5 rounded">Envoyé</span>{{end}}
|
||||
{{if .GuildID}}
|
||||
{{$gname := index $.GuildNames .GuildID}}
|
||||
<span class="text-xs text-indigo-300 bg-indigo-900/30 px-2 py-0.5 rounded">{{if $gname}}{{$gname}}{{else}}{{.GuildID}}{{end}}</span>
|
||||
{{end}}
|
||||
</div>
|
||||
<p class="text-sm text-gray-400 truncate">{{.EmbedTitle}}</p>
|
||||
{{if .ChannelID.Valid}}<p class="text-xs text-gray-600 mt-0.5">Channel : {{.ChannelID.String}}</p>{{end}}
|
||||
</div>
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center gap-2 flex-shrink-0">
|
||||
<a href="/panels/{{.ID}}/edit" class="px-3 py-1.5 text-sm text-indigo-400 hover:text-indigo-300 border border-indigo-700/50 hover:border-indigo-500 rounded-lg transition-colors">
|
||||
Éditer
|
||||
</a>
|
||||
<form method="POST" action="/panels/{{.ID}}/duplicate">
|
||||
<input type="hidden" name="csrf_token" value="{{$.CSRFToken}}">
|
||||
<button type="submit" class="px-3 py-1.5 text-sm text-gray-400 hover:text-gray-200 border border-gray-700 hover:border-gray-500 rounded-lg transition-colors">
|
||||
Dupliquer
|
||||
</button>
|
||||
</form>
|
||||
<button onclick="confirmDelete({{.ID}}, '{{.Name}}')"
|
||||
class="px-3 py-1.5 text-sm text-red-400 hover:text-red-300 border border-red-900/50 hover:border-red-700 rounded-lg transition-colors">
|
||||
Supprimer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="text-center py-20">
|
||||
<p class="text-gray-500 mb-4">Aucun panel configuré.</p>
|
||||
<a href="/panels/new" class="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white text-sm font-medium rounded-lg transition-colors">
|
||||
Créer votre premier panel
|
||||
</a>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<!-- Delete confirmation modal -->
|
||||
<div id="deleteModal" class="hidden fixed inset-0 bg-black/60 flex items-center justify-center z-50">
|
||||
<div class="bg-discord-card border border-discord-border rounded-xl p-6 w-full max-w-sm mx-4">
|
||||
<h3 class="text-white font-semibold mb-2">Supprimer le panel</h3>
|
||||
<p class="text-gray-400 text-sm mb-6">
|
||||
Supprimer <strong id="deleteModalName" class="text-white"></strong> ? Cette action est irréversible.
|
||||
</p>
|
||||
<div class="flex gap-3 justify-end">
|
||||
<button onclick="document.getElementById('deleteModal').classList.add('hidden')"
|
||||
class="px-4 py-2 text-sm text-gray-400 hover:text-white border border-gray-700 rounded-lg transition-colors">
|
||||
Annuler
|
||||
</button>
|
||||
<form id="deleteForm" method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{$.CSRFToken}}">
|
||||
<button type="submit" class="px-4 py-2 text-sm bg-red-600 hover:bg-red-500 text-white rounded-lg transition-colors">
|
||||
Supprimer
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function confirmDelete(id, name) {
|
||||
document.getElementById('deleteModalName').textContent = name;
|
||||
document.getElementById('deleteForm').action = '/panels/' + id + '/delete';
|
||||
document.getElementById('deleteModal').classList.remove('hidden');
|
||||
}
|
||||
document.getElementById('deleteModal').addEventListener('click', function(e) {
|
||||
if (e.target === this) this.classList.add('hidden');
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
@@ -0,0 +1,30 @@
|
||||
{{define "content"}}
|
||||
<h2 class="text-xl font-semibold text-white mb-1">Créer un mot de passe</h2>
|
||||
<p class="text-gray-400 text-sm mb-6">
|
||||
Choisissez un mot de passe sécurisé (12 caractères minimum). Il vous sera demandé à chaque connexion.
|
||||
</p>
|
||||
|
||||
{{if .Error}}
|
||||
<div class="mb-4 px-4 py-3 rounded bg-red-900/50 border border-red-700 text-red-300 text-sm">
|
||||
{{.Error}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<form method="POST" action="/auth/password-setup">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm text-gray-300 mb-1">Mot de passe</label>
|
||||
<input type="password" name="password" minlength="12" required autofocus
|
||||
class="w-full px-3 py-2 bg-gray-800 border border-gray-600 rounded-lg text-white focus:outline-none focus:border-indigo-500"
|
||||
placeholder="12 caractères minimum">
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm text-gray-300 mb-1">Confirmer le mot de passe</label>
|
||||
<input type="password" name="confirm" required
|
||||
class="w-full px-3 py-2 bg-gray-800 border border-gray-600 rounded-lg text-white focus:outline-none focus:border-indigo-500">
|
||||
</div>
|
||||
<button type="submit"
|
||||
class="w-full px-4 py-2.5 bg-indigo-600 hover:bg-indigo-500 text-white font-medium rounded-lg transition-colors">
|
||||
Définir le mot de passe
|
||||
</button>
|
||||
</form>
|
||||
{{end}}
|
||||
@@ -0,0 +1,90 @@
|
||||
{{define "title"}}Sessions{{end}}
|
||||
{{define "header"}}Sessions actives{{end}}
|
||||
{{define "content"}}
|
||||
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<p class="text-gray-400 text-sm">{{len .Sessions}} session{{if gt (len .Sessions) 1}}s{{end}} active{{if gt (len .Sessions) 1}}s{{end}}</p>
|
||||
<button onclick="document.getElementById('revokeAllModal').classList.remove('hidden')"
|
||||
class="px-4 py-2 bg-red-700 hover:bg-red-600 text-white text-sm font-medium rounded-lg transition-colors">
|
||||
Révoquer toutes les autres
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="bg-discord-card rounded-lg border border-discord-border overflow-hidden">
|
||||
{{if .Sessions}}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="text-xs text-gray-400 uppercase border-b border-discord-border">
|
||||
<th class="px-4 py-2 text-left">Admin</th>
|
||||
<th class="px-4 py-2 text-left">IP</th>
|
||||
<th class="px-4 py-2 text-left">User Agent</th>
|
||||
<th class="px-4 py-2 text-left">Dernière activité</th>
|
||||
<th class="px-4 py-2 text-left">Créée le</th>
|
||||
<th class="px-4 py-2 text-right">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-discord-border">
|
||||
{{range .Sessions}}
|
||||
<tr class="hover:bg-white/5 transition-colors {{if .IsCurrent}}bg-indigo-900/20{{end}}">
|
||||
<td class="px-4 py-2 text-white">
|
||||
{{.DiscordUsername}}
|
||||
{{if .IsCurrent}}<span class="ml-1 text-xs text-indigo-400">(vous)</span>{{end}}
|
||||
</td>
|
||||
<td class="px-4 py-2 text-gray-400 font-mono text-xs">{{.IPAddress}}</td>
|
||||
<td class="px-4 py-2 text-gray-500 text-xs max-w-[200px] truncate" title="{{.UserAgent}}">{{.UserAgent}}</td>
|
||||
<td class="px-4 py-2 text-gray-400 text-xs whitespace-nowrap">{{.LastActivity.Format "02/01/2006 15:04:05"}}</td>
|
||||
<td class="px-4 py-2 text-gray-400 text-xs whitespace-nowrap">{{.CreatedAt.Format "02/01/2006 15:04:05"}}</td>
|
||||
<td class="px-4 py-2 text-right">
|
||||
{{if not .IsCurrent}}
|
||||
<form method="POST" action="/sessions/{{.ID}}/revoke">
|
||||
<input type="hidden" name="csrf_token" value="{{$.CSRFToken}}">
|
||||
<button type="submit"
|
||||
class="px-3 py-1 text-xs text-red-400 hover:text-red-300 border border-red-900/50 hover:border-red-700 rounded transition-colors">
|
||||
Révoquer
|
||||
</button>
|
||||
</form>
|
||||
{{else}}
|
||||
<span class="text-xs text-gray-600">Session courante</span>
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="px-5 py-12 text-center text-gray-500 text-sm">
|
||||
Aucune session active.
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<!-- Revoke All Modal -->
|
||||
<div id="revokeAllModal" class="hidden fixed inset-0 bg-black/60 flex items-center justify-center z-50">
|
||||
<div class="bg-discord-card border border-discord-border rounded-xl p-6 w-full max-w-sm mx-4">
|
||||
<h3 class="text-white font-semibold mb-2">Révoquer toutes les autres sessions</h3>
|
||||
<p class="text-gray-400 text-sm mb-6">
|
||||
Cette action déconnectera toutes les sessions actives sauf la vôtre. Cette action est irréversible.
|
||||
</p>
|
||||
<div class="flex gap-3 justify-end">
|
||||
<button onclick="document.getElementById('revokeAllModal').classList.add('hidden')"
|
||||
class="px-4 py-2 text-sm text-gray-400 hover:text-white border border-gray-700 rounded-lg transition-colors">
|
||||
Annuler
|
||||
</button>
|
||||
<form method="POST" action="/sessions/revoke-all">
|
||||
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
||||
<button type="submit" class="px-4 py-2 text-sm bg-red-600 hover:bg-red-500 text-white rounded-lg transition-colors">
|
||||
Révoquer tout
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('revokeAllModal').addEventListener('click', function(e) {
|
||||
if (e.target === this) this.classList.add('hidden');
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
@@ -0,0 +1,35 @@
|
||||
{{define "content"}}
|
||||
<h2 class="text-xl font-semibold text-white mb-1">Configuration TOTP</h2>
|
||||
<p class="text-gray-400 text-sm mb-6">
|
||||
Scanne le QR code avec Google Authenticator ou une app compatible, puis saisis le code pour valider.
|
||||
</p>
|
||||
|
||||
{{if .Error}}
|
||||
<div class="mb-4 px-4 py-3 rounded bg-red-900/50 border border-red-700 text-red-300 text-sm">
|
||||
{{.Error}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="flex justify-center mb-4">
|
||||
<img src="{{.QRCodeBase64}}" alt="QR Code TOTP" class="w-48 h-48 rounded-lg border border-gray-600">
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<p class="text-xs text-gray-400 mb-1">Secret manuel :</p>
|
||||
<code class="block bg-gray-800 px-3 py-2 rounded text-sm text-gray-200 font-mono break-all select-all">{{.Secret}}</code>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="/auth/totp-setup">
|
||||
<input type="hidden" name="secret" value="{{.Secret}}">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm text-gray-300 mb-1">Code de vérification</label>
|
||||
<input type="text" name="code" inputmode="numeric" autocomplete="one-time-code" maxlength="6"
|
||||
class="w-full px-3 py-2 bg-gray-800 border border-gray-600 rounded-lg text-white text-center text-xl tracking-widest focus:outline-none focus:border-indigo-500"
|
||||
placeholder="000000" required autofocus>
|
||||
</div>
|
||||
<button type="submit"
|
||||
class="w-full px-4 py-2.5 bg-indigo-600 hover:bg-indigo-500 text-white font-medium rounded-lg transition-colors">
|
||||
Valider et continuer
|
||||
</button>
|
||||
</form>
|
||||
{{end}}
|
||||
@@ -0,0 +1,41 @@
|
||||
{{define "content"}}
|
||||
<h2 class="text-xl font-semibold text-white mb-1">Vérification</h2>
|
||||
|
||||
{{if .Admin}}
|
||||
<div class="flex items-center gap-3 mb-6 p-3 bg-gray-800 rounded-lg">
|
||||
{{if .Admin.DiscordAvatar}}
|
||||
<img src="https://cdn.discordapp.com/avatars/{{.Admin.DiscordID}}/{{.Admin.DiscordAvatar}}.png?size=64"
|
||||
class="w-10 h-10 rounded-full" alt="">
|
||||
{{end}}
|
||||
<div>
|
||||
<p class="text-sm text-gray-400">Connecté en tant que</p>
|
||||
<p class="font-medium text-white">{{.Admin.DiscordUsername}}</p>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .Error}}
|
||||
<div class="mb-4 px-4 py-3 rounded bg-red-900/50 border border-red-700 text-red-300 text-sm">
|
||||
{{.Error}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<form method="POST" action="/auth/verify">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm text-gray-300 mb-1">Mot de passe</label>
|
||||
<input type="password" name="password" autocomplete="current-password"
|
||||
class="w-full px-3 py-2 bg-gray-800 border border-gray-600 rounded-lg text-white focus:outline-none focus:border-indigo-500"
|
||||
required autofocus>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm text-gray-300 mb-1">Code TOTP</label>
|
||||
<input type="text" name="totp" inputmode="numeric" autocomplete="one-time-code" maxlength="6"
|
||||
class="w-full px-3 py-2 bg-gray-800 border border-gray-600 rounded-lg text-white text-center text-xl tracking-widest focus:outline-none focus:border-indigo-500"
|
||||
placeholder="000000" required>
|
||||
</div>
|
||||
<button type="submit"
|
||||
class="w-full px-4 py-2.5 bg-indigo-600 hover:bg-indigo-500 text-white font-medium rounded-lg transition-colors">
|
||||
Se connecter
|
||||
</button>
|
||||
</form>
|
||||
{{end}}
|
||||
@@ -0,0 +1,170 @@
|
||||
{{define "title"}}Transcripts{{end}}
|
||||
{{define "header"}}Transcripts{{end}}
|
||||
{{define "content"}}
|
||||
|
||||
<!-- Filters -->
|
||||
<form method="GET" action="/transcripts" class="bg-discord-card rounded-lg border border-discord-border p-4 mb-6">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-3 mb-3">
|
||||
<div>
|
||||
<label class="block text-xs text-gray-400 mb-1">Recherche</label>
|
||||
<input type="text" name="search" value="{{.FSearch}}" placeholder="Titre, user ID…"
|
||||
class="w-full bg-discord-hover border border-discord-border rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-discord-accent">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs text-gray-400 mb-1">Statut</label>
|
||||
<select name="status" class="w-full bg-discord-hover border border-discord-border rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:border-discord-accent">
|
||||
<option value="">Tous</option>
|
||||
<option value="open" {{if eq .FStatus "open"}}selected{{end}}>Ouvert</option>
|
||||
<option value="claimed" {{if eq .FStatus "claimed"}}selected{{end}}>Claimé</option>
|
||||
<option value="closed" {{if eq .FStatus "closed"}}selected{{end}}>Fermé</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs text-gray-400 mb-1">Type</label>
|
||||
<select name="type" class="w-full bg-discord-hover border border-discord-border rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:border-discord-accent">
|
||||
<option value="">Tous les types</option>
|
||||
{{range .Types}}
|
||||
<option value="{{.}}" {{if eq . $.FType}}selected{{end}}>{{.}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs text-gray-400 mb-1">Staff</label>
|
||||
<select name="staff" class="w-full bg-discord-hover border border-discord-border rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:border-discord-accent">
|
||||
<option value="">Tous les staffs</option>
|
||||
{{range .StaffList}}
|
||||
<option value="{{.}}" {{if eq . $.FStaff}}selected{{end}}>{{.}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{{if .Guilds}}
|
||||
<div>
|
||||
<label class="block text-xs text-gray-400 mb-1">Serveur</label>
|
||||
<select name="guild" class="w-full bg-discord-hover border border-discord-border rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:border-discord-accent">
|
||||
<option value="">Tous les serveurs</option>
|
||||
{{range .Guilds}}
|
||||
<option value="{{.ID}}" {{if eq .ID $.FGuild}}selected{{end}}>{{.Name}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div>
|
||||
<label class="block text-xs text-gray-400 mb-1">Du</label>
|
||||
<input type="date" name="from" value="{{.FFrom}}"
|
||||
class="w-full bg-discord-hover border border-discord-border rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:border-discord-accent">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs text-gray-400 mb-1">Au</label>
|
||||
<input type="date" name="to" value="{{.FTo}}"
|
||||
class="w-full bg-discord-hover border border-discord-border rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:border-discord-accent">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<button type="submit"
|
||||
class="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white text-sm font-medium rounded-lg transition-colors">
|
||||
Filtrer
|
||||
</button>
|
||||
<a href="/transcripts" class="px-4 py-2 text-sm text-gray-400 hover:text-white border border-discord-border rounded-lg transition-colors">
|
||||
Réinitialiser
|
||||
</a>
|
||||
<span class="text-xs text-gray-500 ml-auto">{{.Total}} résultat{{if gt .Total 1}}s{{end}}</span>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Tickets table -->
|
||||
<div class="bg-discord-card rounded-lg border border-discord-border overflow-hidden">
|
||||
{{if .Tickets}}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="text-xs text-gray-400 uppercase border-b border-discord-border">
|
||||
<th class="px-4 py-2 text-left">#</th>
|
||||
<th class="px-4 py-2 text-left">Serveur</th>
|
||||
<th class="px-4 py-2 text-left">Type</th>
|
||||
<th class="px-4 py-2 text-left">Utilisateur</th>
|
||||
<th class="px-4 py-2 text-left">Staff</th>
|
||||
<th class="px-4 py-2 text-left">Titre</th>
|
||||
<th class="px-4 py-2 text-left">Statut</th>
|
||||
<th class="px-4 py-2 text-left">Ouvert le</th>
|
||||
<th class="px-4 py-2 text-left">Fermé le</th>
|
||||
<th class="px-4 py-2 text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-discord-border">
|
||||
{{range .Tickets}}
|
||||
<tr class="hover:bg-white/5 transition-colors">
|
||||
<td class="px-4 py-2 text-indigo-400 font-mono">{{.TicketNumber}}</td>
|
||||
<td class="px-4 py-2 text-xs text-indigo-300">
|
||||
{{if .GuildID}}{{$gn := index $.GuildNames .GuildID}}{{if $gn}}{{$gn}}{{else}}{{.GuildID}}{{end}}{{else}}<span class="text-gray-600">—</span>{{end}}
|
||||
</td>
|
||||
<td class="px-4 py-2 text-gray-300">{{.Type}}</td>
|
||||
<td class="px-4 py-2 text-gray-400 font-mono text-xs">{{.UserID}}</td>
|
||||
<td class="px-4 py-2 text-gray-400 font-mono text-xs">
|
||||
{{if .ClaimedBy.Valid}}{{.ClaimedBy.String}}{{else}}—{{end}}
|
||||
</td>
|
||||
<td class="px-4 py-2 text-white max-w-[200px] truncate">
|
||||
{{if .TicketTitle.Valid}}{{.TicketTitle.String}}{{else}}<span class="text-gray-500">—</span>{{end}}
|
||||
</td>
|
||||
<td class="px-4 py-2">
|
||||
{{if eq .Status "open"}}
|
||||
<span class="px-2 py-0.5 rounded text-xs bg-green-900/40 text-green-400 border border-green-800">Ouvert</span>
|
||||
{{else if eq .Status "claimed"}}
|
||||
<span class="px-2 py-0.5 rounded text-xs bg-yellow-900/40 text-yellow-400 border border-yellow-800">Claimé</span>
|
||||
{{else}}
|
||||
<span class="px-2 py-0.5 rounded text-xs bg-gray-700 text-gray-400">Fermé</span>
|
||||
{{end}}
|
||||
</td>
|
||||
<td class="px-4 py-2 text-gray-400 text-xs whitespace-nowrap">{{.OpenedAt.Format "02/01/06 15:04"}}</td>
|
||||
<td class="px-4 py-2 text-gray-400 text-xs whitespace-nowrap">
|
||||
{{if .ClosedAt.Valid}}{{.ClosedAt.Time.Format "02/01/06 15:04"}}{{else}}—{{end}}
|
||||
</td>
|
||||
<td class="px-4 py-2 text-right">
|
||||
<div class="flex items-center gap-1 justify-end">
|
||||
{{if .TranscriptPath.Valid}}
|
||||
<a href="/transcripts/view/{{.ID}}" target="_blank"
|
||||
class="px-2 py-1 text-xs text-indigo-400 hover:text-indigo-300 border border-indigo-700/50 hover:border-indigo-500 rounded transition-colors">
|
||||
Voir
|
||||
</a>
|
||||
<a href="/transcripts/download/{{.ID}}"
|
||||
class="px-2 py-1 text-xs text-gray-400 hover:text-gray-200 border border-gray-700 hover:border-gray-500 rounded transition-colors">
|
||||
↓
|
||||
</a>
|
||||
{{else}}
|
||||
<span class="text-xs text-gray-600">—</span>
|
||||
{{end}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{if gt .Pages 1}}
|
||||
<div class="px-5 py-3 border-t border-discord-border flex items-center gap-2 text-sm">
|
||||
{{if gt .Page 1}}
|
||||
<a href="?page={{add .Page -1}}&status={{.FStatus}}&type={{.FType}}&staff={{.FStaff}}&guild={{.FGuild}}&from={{.FFrom}}&to={{.FTo}}&search={{.FSearch}}"
|
||||
class="px-3 py-1 rounded border border-discord-border text-gray-300 hover:text-white hover:border-gray-500 transition-colors">← Précédent</a>
|
||||
{{end}}
|
||||
<span class="text-gray-400">Page {{.Page}} / {{.Pages}}</span>
|
||||
{{if lt .Page .Pages}}
|
||||
<a href="?page={{add .Page 1}}&status={{.FStatus}}&type={{.FType}}&staff={{.FStaff}}&guild={{.FGuild}}&from={{.FFrom}}&to={{.FTo}}&search={{.FSearch}}"
|
||||
class="px-3 py-1 rounded border border-discord-border text-gray-300 hover:text-white hover:border-gray-500 transition-colors">Suivant →</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{else}}
|
||||
<div class="px-5 py-12 text-center text-gray-500 text-sm">
|
||||
Aucun ticket trouvé pour ces critères.
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user