{{-- Aperçu — détail d'un élément, volet compact. Common sections (titre, statut, priorité, description, sous-tâches) render for every type; the "Statut / Priorité / Dates" chip row, the checklist, and the assignations block below have type-specific variants — see the `$element->type->code` checks. Built off the "Aperçu — détail d'un élément" mockup Artifact, but text inputs instead of contenteditable (more robust with wire:change than contenteditable+wire:blur), and read-only plain text instead of disabled-looking chips when `$editable` is false. --}} @php $element = $this->element(); $editable = $this->canEdit(); $ancestors = $this->ancestorChain(); @endphp
{{-- `mode="event"` : ici on navigue sans quitter le volet. --}} {{-- Titre --}} @if ($editable) @else

{{ $element->title }}

@endif
@livewire('toggle-favorite', [ 'favoritableType' => \App\Models\ProjectElement::class, 'favoritableId' => $element->id, ], key('fav-element-'.$element->id)) {{ $element->code }} @if ($element->type->code === 'milestone') {{ $this->milestoneKindLabel($element->milestone->milestone_kind) }} @else {{ $element->type->label }} @endif @if($element->completed_at) Terminé le {{ $element->completed_at->translatedFormat('d/m/Y') }} @endif
{{-- Follow / Report / Statut / Priorité / Dates --}}
@livewire('toggle-follow', [ 'followableType' => \App\Models\ProjectElement::class, 'followableId' => $element->id, 'width_adjusted' => 'w-full', ], key('follow-element-'.$element->id))

@if ($element->project_id === null)
Ranger dans un projet @if ($editable) @forelse ($this->rankableProjects() as $project) {{ $project->name }} @empty Aucun projet @endforelse @else À ranger @endif
@endif
Statut @if ($editable) @foreach ($this->availableStatuses() as $status) {{ $status->label }} @endforeach @else {{ $element->status->label }} @endif
Priorité @if ($editable) @foreach ([null, 'urgent', 'high', 'medium', 'low'] as $priority) @php $priorityArg = $priority === null ? 'null' : "'".$priority."'"; @endphp {{ $this->priorityLabel($priority) }} @endforeach @else @if ($element->priority) {{ $this->priorityLabel($element->priority) }} @endif @endif
@if ($element->type->code !== 'meeting')
Début @if ($editable) $element->start_date != null, 'text-gray-300 dark:text-gray-600' => $element->start_date == null, ]) /> @else {{ $element->start_date?->translatedFormat('d/m/Y') ?? '—' }} @endif
Echéance @if ($editable) $element->due_date != null, 'text-gray-300 dark:text-gray-600' => $element->due_date == null, ]) /> @else {{ $element->due_date?->translatedFormat('d/m/Y') ?? '—' }} @endif
@else @if ($element->meeting) @if ($editable && ! $element->meeting->outlook_event_id)
Début $element->meeting->starts_at != null, 'text-gray-300 dark:text-gray-600' => $element->meeting->starts_at == null, ]) />
Fin $element->meeting->ends_at != null, 'text-gray-300 dark:text-gray-600' => $element->meeting->ends_at == null, ]) />
@else
Horaire {{ $element->meeting->starts_at?->translatedFormat('j M · H:i') }} – {{ $element->meeting->ends_at?->translatedFormat('H:i') }} @if ($element->meeting->outlook_event_id) Synchronisée depuis Outlook @endif
@endif @endif @endif @if ($this->isMisaligned()) Dates désalignées @endif
{{-- ACCES REUNIONS --}}
{{-- Réunion : l'horaire s'édite dans le bloc Début/Échéance ci-dessus (updateMeetingStartsAt()/updateMeetingEndsAt()) — ce badge n'en est qu'un résumé compact, toujours en lecture seule, affiché ici aux côtés du lieu et du lien de visio. --}} @if ($element->type->code === 'meeting' && $element->meeting) {{ $element->meeting->starts_at?->translatedFormat('j M · H:i') }} – {{ $element->meeting->ends_at?->translatedFormat('H:i') }} @if ($element->meeting->is_cancelled) Annulée @endif @if ($element->meeting->location) {{ $element->meeting->location }} @endif @if ($element->meeting->online_meeting_url) Rejoindre la visio @endif @endif
{{-- Description — stored as HTML (ProjectElementForm's field is a RichEditor), but this first pass edits it as plain text (matching the mockup's plain contenteditable, not a WYSIWYG toolbar): existing HTML is rendered properly for reading, but stripped down to plain text as soon as it's edited here, which flattens any formatting. Fine as a v1 simplification, not as the final behaviour. --}}
Description
@if ($editable) @elseif ($element->description) {{-- Sanitized at render time — see project-detail.blade.php's note on why the raw RichEditor payload is never trusted. No @tailwindcss/typography plugin installed — relies on the browser's own default styling for

/

    //etc. --}}
    {!! \Illuminate\Support\Str::sanitizeHtml($element->description) !!}
    @else

    Aucune description.

    @endif
{{-- FIN DES ATTRIBUTS COMMUNS --}}
{{-- ATTRIBUTS PARTICULIERS AU TYPE D' --}} {{-- TASK --}} {{-- Checklist --}} @if ($element->type->code === 'task' && $element->task) @php $checklist = $element->task->checklist ?? []; $checklistTotal = count($checklist); $checklistDone = collect($checklist)->where('done', true)->count(); @endphp
Checklist @if ($checklistTotal > 0) {{ $checklistDone }}/{{ $checklistTotal }} @endif
@if ($checklistTotal > 0)
@endif
@foreach ($checklist as $index => $item)
$item['done'], 'text-gray-950 dark:text-white' => ! $item['done'], ]) > {{ $item['label'] }} @if ($editable) @endif
@endforeach
@if ($editable)
{{-- Coller une liste (plusieurs lignes) crée une entrée de checklist par ligne — voir addChecklistItemsFromPastedList(). Un collage d'une seule ligne suit le chemin normal (remplit le champ, Entrée valide comme d'habitude). --}}
@endif
@endif {{-- FIN TASK --}} {{-- Jalon / Livrable / Objectif --}} @if ($element->type->code === 'milestone' && $element->milestone) @php $milestone = $element->milestone; @endphp
Validation
@if ($editable) @foreach (['jalon', 'livrable', 'objectif'] as $kind) {{ $this->milestoneKindLabel($kind) }} @endforeach @else {{ $this->milestoneKindLabel($milestone->milestone_kind) }} @endif {{-- Jalon majeur (JOS-192) : ceux que la frise du tableau de bord projet affiche. --}} @if ($editable) @elseif ($milestone->is_major) Jalon majeur @endif @if ($milestone->validated_at) Validé{{ $milestone->validator ? ' par '.$milestone->validator->full_name : '' }} le {{ $milestone->validated_at->translatedFormat('j M') }} @endif @if ($editable) @endif
Critères de validation
@if ($editable) @elseif ($milestone->validation_criteria)

{{ $milestone->validation_criteria }}

@else

Aucun critère renseigné.

@endif
@endif {{-- FIN Jalon / Livrable / Objectif --}} {{-- CHANTIER --}} @if ($element->type->code === 'chantier' && $element->chantier) @php $chantier = $element->chantier; @endphp
Couleur @if ($editable)
@foreach ($this->chantierColors() as $option) @endforeach
@if ($element->chantier->color !== null) @endif
@else {{ $element->chantier->color->label() }} @endif
Note budgétaire @if ($editable) @elseif ($chantier->budget_note)

{{ $chantier->budget_note }}

@else

Aucune note budgétaire.

@endif
{{-- Risques (JOS-164, lot 3) — voir/ajouter ici, modifier depuis l'onglet Risques du projet. --}}
Risques @if ($editable) @endif
@php $chantierRisks = $this->chantierRisks(); @endphp @if ($chantierRisks->isEmpty())

Aucun risque identifié pour l'instant.

@else
@foreach ($chantierRisks as $risk) @php $level = $risk->level(); @endphp
{{ $risk->score() }} {{ $risk->title }}
@endforeach
@endif
{{-- SWOT (JOS-164) — voir/ajouter ici, modifier depuis l'onglet SWOT du projet. --}}
SWOT @php $chantierSwot = $this->chantierSwotEntries(); @endphp
@foreach (\App\Services\Swot\SwotQuadrant::cases() as $quadrant)
headingClasses()])>{{ $quadrant->getLabel() }} @if ($editable) @endif
@forelse ($chantierSwot[$quadrant->value] as $entry)

{{ $entry->content }}

@empty

—

@endforelse
@endforeach
@endif {{-- Sous-tâches --}} @if ($element->type->allows_children)
Sous-tâches @if ($element->children->isNotEmpty()) {{ $element->children->where('status.category', 'done')->count() }}/{{ $element->children->count() }} @endif
@foreach ($element->children as $child) @php $isDone = $child->status->category === 'done'; @endphp @endforeach
@if ($editable)
@endif
@endif {{-- Réunion : Organisateur + Participants plutôt que le bloc RACI ci- dessous — une réunion n'a pas d'"assignés" à proprement parler, et `$element->assignees` reste vide pour ce type dans ProjectElementForm (pas de section "Assignations" affichée quand on édite une réunion). --}} @if ($element->type->code === 'meeting' && $element->meeting) @php $seriesPosition = $this->seriesPosition(); @endphp
@if ($element->meeting->series_id !== null)
Réunion récurrente ({{ $seriesPosition[0] }}/{{ $seriesPosition[1] }}) @if ($editable && $element->meeting->outlook_event_id === null) @endif
@elseif ($editable && $element->meeting->starts_at !== null && $element->meeting->outlook_event_id === null)
@endif @if ($element->meeting->starts_at !== null)
@endif {{-- Distinct de la Description ci-dessus : celle-ci dit à quoi sert la réunion (commun à une série), l'ordre du jour dit ce qu'on y traite (propre à chaque occurrence) — voir CLAUDE.md. --}}
Ordre du jour
@if ($editable) @elseif ($element->meeting->agenda)

{{ $element->meeting->agenda }}

@else

Aucun ordre du jour.

@endif
@if ($editable && $element->meeting->outlook_event_id === null)
Lieu
Lien visio
@endif
Organisateur
@if ($editable && $element->meeting->outlook_event_id === null) @elseif ($element->meeting->organizer)
{{ $element->meeting->organizer->initials }} {{ $element->meeting->organizer->full_name }}
@else

—

@endif
Participants @if ($editable && $element->meeting->series_id !== null) @endif
@foreach ($element->meeting->attendees as $attendee)
{{ $attendee->person->initials ?? mb_strtoupper(mb_substr($attendee->display_name ?? $attendee->email ?? '?', 0, 1)) }} {{ $attendee->person->full_name ?? $attendee->display_name ?? $attendee->email }} @if ($attendee->attendee_type === 'optional') Optionnel @endif @if ($editable) @endif
@endforeach @if ($element->meeting->attendees->isEmpty())

Aucun participant.

@endif
@if ($editable)
@if (trim($attendeeSearch) !== '')
@foreach ($this->searchAttendeeCandidates($attendeeSearch) as $candidate) @endforeach
@endif
@endif
@endif
{{-- Assignations RACI — même popover/logique que view-project-element.blade.php (backend partagé via HasElementDetail), pour ne jamais avoir à maintenir deux implémentations de la même fonctionnalité. Le "+" et le popover d'édition ne s'affichent que si `$editable` ; en lecture seule, la liste des assignés reste visible sans le picker. --}} @if ($element->type->code !== 'meeting')
Assignations
@foreach ($element->assignees as $assignee)
{{ $assignee->initials }} {{ mb_strtoupper(mb_substr($assignee->pivot->role, 0, 1)) }}
@endforeach @if ($editable) {{-- `x-filament::dropdown` (floating-ui, `flip` on by default) plutôt qu'un `
` positionné en `top-full` : ce dernier sortait de l'écran — ou du panel Aperçu, qui défile — dès que le déclencheur était en bas, obligeant à défiler pour voir le popover. `wire:key` est **obligatoire** ici, pas cosmétique : c'est lui qui fait poser `wire:ignore.self` sur le panneau (voir dropdown/index.blade.php du package). Sans lui, la recherche (`wire:model.live` juste en dessous) déclenche une requête à chaque frappe, et le morph Livewire qui suit réécrit le panneau sur son rendu serveur — `display: none` — fermant le popover au premier caractère tapé. Régression réelle, repérée après coup : jamais testée frappe-au-clavier pendant que le popover était ouvert. --}} {{-- Le vrai responsable du saut vers le haut, trouvé en lisant `public/vendor/livewire/livewire.js` : le panel Aperçu est piégé par un focus-trap (bibliothèque `focus-trap`, embarquée par le plugin focus d'Alpine que Livewire empaquette). Ce piège observe le DOM (`MutationObserver`) et, dès que le nœud qu'il croit "le plus récemment focus" disparaît, refocalise de force le premier élément « focusable » du panel — near le haut de la modale. Cliquer un nom ici focus le bouton (comportement par défaut de Chrome/Firefox sur un `
@elseif ($pendingAssigneeId !== null) {{-- L'étape de confirmation du rôle avant écriture : cliquer un nom ne notifie plus personne tout seul, il ouvre juste ce choix — une seule écriture (donc une seule notification) part au clic "Ajouter". --}} @php $pendingAssignee = $this->pendingAssignee(); @endphp
{{ $pendingAssignee?->initials }} {{ $pendingAssignee?->full_name }}
@else @php $assigneeResults = $this->searchPeople($assigneeSearch ?? ''); @endphp @foreach ($assigneeResults as $person) @endforeach @if ($assigneeResults->isEmpty() && trim($assigneeSearch ?? '') !== '') @endif @endif @if ($element->assignees->isNotEmpty())
@foreach ($element->assignees as $assignee)
{{ $assignee->initials }} {{ $assignee->full_name }}
@endforeach @endif
R = Responsable · A = Approbateur · C = Consulté · I = Informé
@endif
@if ($element->assignees->isNotEmpty())
{{ $element->assignees->map(fn ($a) => $a->full_name.' ('.$this->raciLabel($a->pivot->role).')')->join(', ') }}
@endif
@endif
Charge estimée
@if ($element->type->code === 'milestone' || $element->type->code === 'task' ) @if ($editable)
heures
@else {{ $element->estimated_effort_hours() !== null ? $element->estimated_effort_hours().' heures' : '—' }} @endif @else {{ $element->estimated_effort_hours() !== null ? $element->estimated_effort_hours().' heures' : '—' }} @endif
{{-- Dépendances — même écriture (DependencyLinker) et même détection de violation que le MCP, voir HasElementDetail. « Dépend de » se crée depuis ici ; « Bloque » ne se retire que — pour en ajouter un, ouvrir l'autre élément et y poser son propre prérequis. --}}
Dépend de @if ($editable && $element->project_id !== null)
@if ($pendingDependencyId !== null) @php $pendingDependency = $this->pendingDependencyCandidate(); @endphp
{{ $pendingDependency?->title }}
@else @php $dependencyResults = $this->searchDependencyCandidates($dependencySearch ?? ''); @endphp @forelse ($dependencyResults as $candidate) @empty

Aucun résultat

@endforelse @endif
@endif
@php $elementDependencies = $this->dependencies(); @endphp @forelse ($elementDependencies as $dependency)
$dependency->isViolated(), 'border-gray-200 dark:border-white/10' => ! $dependency->isViolated(), ])> @if ($dependency->isViolated()) @else @endif
{{ $dependency->prerequisite->title }}
{{ $dependency->prerequisite->code }} · {{ $this->dependencyTypeLabel($dependency->dependency_type) }}@if ($dependency->lag_days) · +{{ $dependency->lag_days }}j @endif
@if ($editable) @endif
@empty

Aucune dépendance.

@endforelse @php $elementDependents = $this->dependents(); @endphp @if ($elementDependents->isNotEmpty()) Bloque @foreach ($elementDependents as $dependency)
{{ $dependency->element->title }}
{{ $dependency->element->code }} · {{ $this->dependencyTypeLabel($dependency->dependency_type) }}
@if ($editable) @endif
@endforeach @endif
{{-- Pas le fil complet ici — juste un compteur, qui renvoie vers la page dédiée où App\Livewire\Comments\CommentThread affiche/édite vraiment les commentaires. `elementExpandUrl()` vient de HasApercu (déjà utilisé pour le bouton "agrandir" du panel), pas dupliqué ici. --}} @php $commentsCount = $this->commentsCount(); @endphp @if ($commentsCount > 0) {{ $commentsCount }} {{ $commentsCount > 1 ? 'commentaires' : 'commentaire' }} @endif @if ($element->type->code === 'chantier') @include('livewire.apercu.partials.add-chantier-risk-modal') @include('livewire.apercu.partials.add-chantier-swot-modal') @endif @include('livewire.apercu.partials.meeting-scope-modal') {{-- Peint le modal de `promoteChecklistItemAction()` — un composant Livewire simple (contrairement à une page Filament) ne le reçoit pas gratuitement de son layout, voir quick-create-task.blade.php pour le même besoin. --}}