tenantId()) ->withCount('assignments') ->orderByDesc('is_default')->orderBy('name') ->get(); return view('admin.templates.index', compact('templates')); } public function create(): View { return view('admin.templates.edit', [ 'template' => new Template([ 'draft_html_full' => file_get_contents(database_path('seeders/templates/agents-full.html')), 'draft_html_short' => file_get_contents(database_path('seeders/templates/agents-short.html')), ]), 'versions' => collect(), 'variables' => VariableMap::FIELDS, 'previewAgent' => $this->defaultPreviewAgent(), ]); } public function store(Request $request): RedirectResponse { $data = $this->validated($request); $template = Template::create($data + ['tenant_id' => $this->tenantId()]); AuditLog::record('admin', $this->actor(), 'template.create', $template, ['name' => $template->name]); return redirect()->route('admin.templates.edit', $template)->with('status', 'Template créé (brouillon non publié).'); } public function edit(Template $template): View { $this->ensureTenant($template); return view('admin.templates.edit', [ 'template' => $template, 'versions' => $template->versions()->with('publisher:id,name')->orderByDesc('version')->limit(20)->get(), 'variables' => VariableMap::FIELDS, 'previewAgent' => $this->defaultPreviewAgent(), ]); } public function update(Request $request, Template $template): RedirectResponse { $this->ensureTenant($template); $data = $this->validated($request); $template->fill($data); $changed = array_keys($template->getDirty()); $template->save(); AuditLog::record('admin', $this->actor(), 'template.update', $template, ['fields' => $changed]); return back()->with('status', 'Brouillon enregistré.'); } /** Aperçu du brouillon en cours de saisie (non enregistré) sur un agent réel. */ public function preview(Request $request, SignatureBuilder $builder): JsonResponse { $data = $request->validate([ 'html' => ['nullable', 'string', 'max:100000'], 'variant' => ['required', 'in:full,short'], 'agent_id' => ['nullable', 'integer'], ]); $agent = isset($data['agent_id']) ? DirectoryUser::where('tenant_id', $this->tenantId())->find($data['agent_id']) : $this->defaultPreviewAgent(); abort_unless($agent, 422, 'Aucun agent pour l\'aperçu.'); $html = $this->sanitizer->clean((string) ($data['html'] ?? '')); return response()->json([ 'html' => $builder->preview($agent, $html, $data['variant']), 'agent' => $agent->display_name.' — '.$agent->mail, 'variables' => (new TemplateRenderer)->variables($html), ]); } public function publish(Template $template): RedirectResponse { $this->ensureTenant($template); abort_if(blank($template->draft_html_full) && blank($template->html_full), 422, 'Le template complet est vide.'); $template->publish(auth()->user()); AuditLog::record('admin', $this->actor(), 'template.publish', $template, ['version' => $template->published_version]); return back()->with('status', "Version {$template->published_version} publiée : elle est servie immédiatement par l'add-in."); } /** Recharge une version publiée dans le brouillon (il faut republier ensuite). */ public function restore(Template $template, TemplateVersion $version): RedirectResponse { $this->ensureTenant($template); abort_unless($version->template_id === $template->id, 404); $template->update(['draft_html_full' => $version->html_full, 'draft_html_short' => $version->html_short]); AuditLog::record('admin', $this->actor(), 'template.restore', $template, ['version' => $version->version]); return back()->with('status', "Version {$version->version} rechargée dans le brouillon. Publiez pour l'appliquer."); } public function makeDefault(Template $template): RedirectResponse { $this->ensureTenant($template); abort_if(blank($template->html_full), 422, 'Publiez le template avant d\'en faire le template par défaut.'); DB::transaction(function () use ($template) { Template::where('tenant_id', $this->tenantId())->update(['is_default' => false]); $template->update(['is_default' => true]); }); AuditLog::record('admin', $this->actor(), 'template.default', $template); return back()->with('status', "« {$template->name} » est maintenant le template par défaut."); } public function destroy(Template $template): RedirectResponse { $this->ensureTenant($template); abort_if($template->is_default, 422, 'Le template par défaut ne peut pas être supprimé.'); AuditLog::record('admin', $this->actor(), 'template.delete', $template, ['name' => $template->name]); $template->delete(); // les affectations sont supprimées en cascade return redirect()->route('admin.templates.index')->with('status', 'Template supprimé.'); } private function validated(Request $request): array { $data = $request->validate([ 'name' => ['required', 'string', 'max:120'], 'description' => ['nullable', 'string', 'max:500'], 'draft_html_full' => ['required', 'string', 'max:100000'], 'draft_html_short' => ['nullable', 'string', 'max:100000'], ]); $data['draft_html_full'] = $this->sanitizer->clean($data['draft_html_full']); $data['draft_html_short'] = isset($data['draft_html_short']) ? $this->sanitizer->clean($data['draft_html_short']) : null; return $data; } /** Agent utilisé par défaut pour l'aperçu : l'administrateur lui-même. */ private function defaultPreviewAgent(): ?DirectoryUser { $user = auth()->user(); return DirectoryUser::where('tenant_id', $user->tenant_id)->where('entra_oid', $user->entra_oid)->first() ?? DirectoryUser::where('tenant_id', $user->tenant_id)->orderBy('display_name')->first(); } }