feat(planning): gestion des statuts automatiques, suppression de spectacles et améliorations d'affichage
- Statuts camions calculés automatiquement selon les shows assignés - Gestion manuelle limitée aux statuts "En panne" et "En maintenance" - Bouton pour supprimer un spectacle directement depuis la grille ou le panneau - Ajustement de la largeur dynamique des évènements et camions dans les créneaux - Nouveau style pour les badges de statut et indices visuels des actions - Indications sur les statuts automatiques dans le formulaire camion
This commit is contained in:
@@ -317,8 +317,6 @@
|
|||||||
/* ── Événements dans la grille ──────────────────────────── */
|
/* ── Événements dans la grille ──────────────────────────── */
|
||||||
.show-event {
|
.show-event {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 3px;
|
|
||||||
right: 3px;
|
|
||||||
height: 42px;
|
height: 42px;
|
||||||
background: linear-gradient(135deg, #d4a574, #c49563);
|
background: linear-gradient(135deg, #d4a574, #c49563);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
@@ -348,8 +346,6 @@
|
|||||||
|
|
||||||
.truck-event {
|
.truck-event {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 3px;
|
|
||||||
right: 3px;
|
|
||||||
height: 42px;
|
height: 42px;
|
||||||
background: linear-gradient(135deg, #605DC8, #4e4aaa);
|
background: linear-gradient(135deg, #605DC8, #4e4aaa);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
@@ -873,3 +869,58 @@
|
|||||||
opacity: 0.45;
|
opacity: 0.45;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Hint dans le formulaire ────────────────────────────── */
|
||||||
|
.form-hint {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #bbb;
|
||||||
|
margin-top: 5px;
|
||||||
|
font-style: italic;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Badge statut camion dans truck-tag ─────────────────── */
|
||||||
|
.truck-tag-statut {
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(96, 93, 200, 0.12);
|
||||||
|
color: #605DC8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Couleurs selon statut ──────────────────────────────── */
|
||||||
|
.statut-disponible {
|
||||||
|
color: #3aaa6e !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statut-indisponible {
|
||||||
|
color: #e05252 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statut-en-déplacement {
|
||||||
|
color: #d4a574 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statut-en-panne {
|
||||||
|
color: #e05252 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statut-en-maintenance {
|
||||||
|
color: #888 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-delete-btn {
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
color: #ccc;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0 2px;
|
||||||
|
transition: color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-delete-btn:hover {
|
||||||
|
color: #e05252;
|
||||||
|
}
|
||||||
|
|||||||
@@ -62,7 +62,6 @@
|
|||||||
<div class="day-date">{{ date.getDate() }}</div>
|
<div class="day-date">{{ date.getDate() }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="day-slots">
|
<div class="day-slots">
|
||||||
<!-- Ligne "maintenant" sur la colonne du jour actuel -->
|
|
||||||
@if (isToday(date)) {
|
@if (isToday(date)) {
|
||||||
<div class="now-line" [style.top.px]="getNowTopPx()"></div>
|
<div class="now-line" [style.top.px]="getNowTopPx()"></div>
|
||||||
}
|
}
|
||||||
@@ -71,16 +70,20 @@
|
|||||||
(click)="selectDay(date)">
|
(click)="selectDay(date)">
|
||||||
@for (show of getShowsForSlot(date, hour); track show.id) {
|
@for (show of getShowsForSlot(date, hour); track show.id) {
|
||||||
<div class="show-event"
|
<div class="show-event"
|
||||||
[style.top.px]="show.date ? getEventTopPx(show.date) : 0">
|
[style.top.px]="show.date ? getEventTopPx(show.date) : 0"
|
||||||
|
[style.left]="'calc(' + (show.colIndex / show.colCount * 100) + '% + 3px)'"
|
||||||
|
[style.right]="'calc(' + ((show.colCount - show.colIndex - 1) / show.colCount * 100) + '% + 3px)'">
|
||||||
<span class="show-event-name">{{ show.name }}</span>
|
<span class="show-event-name">{{ show.name }}</span>
|
||||||
<span class="show-event-time" *ngIf="show.date">{{ formatShowTime(show.date) }}</span>
|
<span class="show-event-time" *ngIf="show.date">{{ formatShowTime(show.date) }}</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@for (truck of getTrucksForSlot(date, hour); track truck.id) {
|
@for (truck of getTrucksForSlot(date, hour); track truck.id) {
|
||||||
<div class="truck-event"
|
<div class="truck-event"
|
||||||
[style.top.px]="truck.showId ? 0 : 0">
|
[style.top.px]="truck.showId ? 0 : 0"
|
||||||
|
[style.left]="'calc(' + (truck.colIndex / truck.colCount * 100) + '% + 3px)'"
|
||||||
|
[style.right]="'calc(' + ((truck.colCount - truck.colIndex - 1) / truck.colCount * 100) + '% + 3px)'">
|
||||||
<span class="truck-event-name">{{ truck.type || 'Camion' }}</span>
|
<span class="truck-event-name">{{ truck.type || 'Camion' }}</span>
|
||||||
<span class="truck-event-statut" *ngIf="truck.statut">{{ truck.statut }}</span>
|
<span class="truck-event-statut">{{ getComputedTruckStatus(truck) }}</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -153,7 +156,10 @@
|
|||||||
<div class="show-card">
|
<div class="show-card">
|
||||||
<div class="show-card-header">
|
<div class="show-card-header">
|
||||||
<strong>{{ show.name }}</strong>
|
<strong>{{ show.name }}</strong>
|
||||||
<span class="show-time" *ngIf="show.date">{{ formatShowTime(show.date) }}</span>
|
<div style="display:flex; align-items:center; gap:6px;">
|
||||||
|
<span class="show-time" *ngIf="show.date">{{ formatShowTime(show.date) }}</span>
|
||||||
|
<button class="show-delete-btn" (click)="deleteShow(show.id!)">×</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p *ngIf="show.place">{{ show.place }}</p>
|
<p *ngIf="show.place">{{ show.place }}</p>
|
||||||
<p *ngIf="show.description">{{ show.description }}</p>
|
<p *ngIf="show.description">{{ show.description }}</p>
|
||||||
@@ -161,6 +167,9 @@
|
|||||||
@for (truck of getTrucksForShow(show.id!); track truck.id) {
|
@for (truck of getTrucksForShow(show.id!); track truck.id) {
|
||||||
<span class="truck-tag">
|
<span class="truck-tag">
|
||||||
{{ truck.type }}
|
{{ truck.type }}
|
||||||
|
<span class="truck-tag-statut" [class]="'statut-' + getComputedTruckStatus(truck).toLowerCase().replace(' ', '-')">
|
||||||
|
{{ getComputedTruckStatus(truck) }}
|
||||||
|
</span>
|
||||||
<button class="truck-edit" (click)="openEditTruckModal(truck)">✎</button>
|
<button class="truck-edit" (click)="openEditTruckModal(truck)">✎</button>
|
||||||
<button class="truck-remove" (click)="removeTruck(truck.id!)">×</button>
|
<button class="truck-remove" (click)="removeTruck(truck.id!)">×</button>
|
||||||
</span>
|
</span>
|
||||||
@@ -200,7 +209,9 @@
|
|||||||
<div class="show-card-header">
|
<div class="show-card-header">
|
||||||
<strong>{{ truck.type || 'Camion' }}</strong>
|
<strong>{{ truck.type || 'Camion' }}</strong>
|
||||||
<div style="display:flex; align-items:center; gap:6px;">
|
<div style="display:flex; align-items:center; gap:6px;">
|
||||||
<span class="truck-statut" *ngIf="truck.statut">{{ truck.statut }}</span>
|
<span class="truck-statut" [class]="'statut-' + getComputedTruckStatus(truck).toLowerCase().replace(' ', '-')">
|
||||||
|
{{ getComputedTruckStatus(truck) }}
|
||||||
|
</span>
|
||||||
<button class="truck-edit-card" (click)="openEditTruckModal(truck)">✎</button>
|
<button class="truck-edit-card" (click)="openEditTruckModal(truck)">✎</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -212,7 +223,6 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -331,16 +341,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Statut : uniquement En panne et En maintenance sont modifiables manuellement -->
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label class="form-label">Statut</label>
|
<label class="form-label">Statut manuel</label>
|
||||||
<select class="form-input" [(ngModel)]="newTruck.status" name="truckStatus">
|
<select class="form-input" [(ngModel)]="newTruck.status" name="truckStatus">
|
||||||
<option value="">-- Choisir un statut --</option>
|
<option value="">-- Automatique --</option>
|
||||||
<option value="Disponible">Disponible</option>
|
|
||||||
<option value="Indisponible">Indisponible</option>
|
|
||||||
<option value="En panne">En panne</option>
|
<option value="En panne">En panne</option>
|
||||||
<option value="En maintenance">En maintenance</option>
|
<option value="En maintenance">En maintenance</option>
|
||||||
<option value="En déplacement">En déplacement</option>
|
|
||||||
</select>
|
</select>
|
||||||
|
<p class="form-hint">Les statuts Disponible, Indisponible et En déplacement sont calculés automatiquement selon le show assigné.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export class Planning implements OnInit, AfterViewInit {
|
|||||||
if (!this.newShow.name) return;
|
if (!this.newShow.name) return;
|
||||||
const dto: PyroFetesDTOShowRequestCreateShowDto = {
|
const dto: PyroFetesDTOShowRequestCreateShowDto = {
|
||||||
...this.newShow,
|
...this.newShow,
|
||||||
date: this.newShowDate ? this.newShowDate.toISOString() : undefined,
|
date: this.newShowDate ? this.formatDateLocal(this.newShowDate) : undefined,
|
||||||
};
|
};
|
||||||
await firstValueFrom(this.showsServices.pyroFetesEndpointsShowCreateShowEndpoint(dto));
|
await firstValueFrom(this.showsServices.pyroFetesEndpointsShowCreateShowEndpoint(dto));
|
||||||
await this.fetchShows();
|
await this.fetchShows();
|
||||||
@@ -94,6 +94,16 @@ export class Planning implements OnInit, AfterViewInit {
|
|||||||
this.isVisible.set(false);
|
this.isVisible.set(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private formatDateLocal(date: Date): string {
|
||||||
|
const pad = (n: number) => n.toString().padStart(2, '0');
|
||||||
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}:00`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteShow(showId: number): Promise<void> {
|
||||||
|
await firstValueFrom(this.showsServices.pyroFetesEndpointsShowDeleteShowEndpoint(showId));
|
||||||
|
await this.fetchShows();
|
||||||
|
}
|
||||||
|
|
||||||
handleCancel(): void {
|
handleCancel(): void {
|
||||||
this.newShow = {};
|
this.newShow = {};
|
||||||
this.newShowDate = null;
|
this.newShowDate = null;
|
||||||
@@ -207,15 +217,43 @@ export class Planning implements OnInit, AfterViewInit {
|
|||||||
this.selectedDay = date;
|
this.selectedDay = date;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Both filters off → show everything. One active → only that type. Both active → both types.
|
// ── Filtres ──────────────────────────────────────────────────
|
||||||
|
// Aucun filtre actif → rien ne s'affiche. Chaque filtre affiche uniquement son type.
|
||||||
get shouldShowShows(): boolean {
|
get shouldShowShows(): boolean {
|
||||||
return !this.isCamionFilterActive || this.isShowFilterActive;
|
return this.isShowFilterActive;
|
||||||
}
|
}
|
||||||
|
|
||||||
get shouldShowCamions(): boolean {
|
get shouldShowCamions(): boolean {
|
||||||
return !this.isShowFilterActive || this.isCamionFilterActive;
|
return this.isCamionFilterActive;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Statut automatique des camions ───────────────────────────
|
||||||
|
// Priorité : En panne / En maintenance (manuel) → toujours respectés
|
||||||
|
// Sinon calculé selon le show associé et l'heure actuelle
|
||||||
|
getComputedTruckStatus(truck: PyroFetesDTOTruckResponseReadTruckDto): string {
|
||||||
|
// Statuts manuels prioritaires, jamais écrasés
|
||||||
|
if (truck.statut === 'En panne' || truck.statut === 'En maintenance') {
|
||||||
|
return truck.statut;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pas de show assigné → disponible
|
||||||
|
if (!truck.showId) return 'Disponible';
|
||||||
|
|
||||||
|
// Cherche le show associé
|
||||||
|
const show = this.shows().find(s => s.id === truck.showId);
|
||||||
|
if (!show?.date) return 'Disponible';
|
||||||
|
|
||||||
|
const showDate = new Date(show.date);
|
||||||
|
const now = new Date();
|
||||||
|
const diffMs = showDate.getTime() - now.getTime();
|
||||||
|
const diffH = diffMs / (1000 * 60 * 60);
|
||||||
|
|
||||||
|
if (diffH < 0) return 'Disponible'; // show passé → camion libre
|
||||||
|
if (diffH < 1) return 'En déplacement'; // moins d'1h avant le show
|
||||||
|
return 'Indisponible'; // show futur (>= 1h)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Slots et grille ──────────────────────────────────────────
|
||||||
getShowsForDay(date: Date): PyroFetesDTOShowResponseReadShowDto[] {
|
getShowsForDay(date: Date): PyroFetesDTOShowResponseReadShowDto[] {
|
||||||
if (!this.shouldShowShows) return [];
|
if (!this.shouldShowShows) return [];
|
||||||
return this.shows().filter(show => {
|
return this.shows().filter(show => {
|
||||||
@@ -227,9 +265,33 @@ export class Planning implements OnInit, AfterViewInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getShowsForSlot(date: Date, hour: string): PyroFetesDTOShowResponseReadShowDto[] {
|
getShowsForSlot(date: Date, hour: string): (PyroFetesDTOShowResponseReadShowDto & { colIndex: number; colCount: number })[] {
|
||||||
if (!this.shouldShowShows) return [];
|
if (!this.shouldShowShows) return [];
|
||||||
const h = hour === '' ? 0 : parseInt(hour);
|
const h = hour === '' ? 0 : parseInt(hour);
|
||||||
|
const shows = this.shows().filter(show => {
|
||||||
|
if (!show.date) return false;
|
||||||
|
const d = new Date(show.date);
|
||||||
|
return d.getFullYear() === date.getFullYear() &&
|
||||||
|
d.getMonth() === date.getMonth() &&
|
||||||
|
d.getDate() === date.getDate() &&
|
||||||
|
d.getHours() === h;
|
||||||
|
});
|
||||||
|
const trucksPresent = this.shouldShowCamions && this.getTrucksRawForSlot(date, h).length > 0;
|
||||||
|
const colCount = trucksPresent ? 2 : 1;
|
||||||
|
return shows.map(s => ({ ...s, colIndex: 0, colCount }));
|
||||||
|
}
|
||||||
|
|
||||||
|
getTrucksForSlot(date: Date, hour: string): (PyroFetesDTOTruckResponseReadTruckDto & { colIndex: number; colCount: number })[] {
|
||||||
|
if (!this.shouldShowCamions) return [];
|
||||||
|
const h = hour === '' ? 0 : parseInt(hour);
|
||||||
|
const trucks = this.getTrucksRawForSlot(date, h);
|
||||||
|
const showsPresent = this.shouldShowShows && this.getShowsRawForSlot(date, h).length > 0;
|
||||||
|
const colCount = showsPresent ? 2 : 1;
|
||||||
|
return trucks.map(t => ({ ...t, colIndex: showsPresent ? 1 : 0, colCount }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// helpers internes
|
||||||
|
private getShowsRawForSlot(date: Date, h: number): PyroFetesDTOShowResponseReadShowDto[] {
|
||||||
return this.shows().filter(show => {
|
return this.shows().filter(show => {
|
||||||
if (!show.date) return false;
|
if (!show.date) return false;
|
||||||
const d = new Date(show.date);
|
const d = new Date(show.date);
|
||||||
@@ -240,9 +302,7 @@ export class Planning implements OnInit, AfterViewInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getTrucksForSlot(date: Date, hour: string): PyroFetesDTOTruckResponseReadTruckDto[] {
|
private getTrucksRawForSlot(date: Date, h: number): PyroFetesDTOTruckResponseReadTruckDto[] {
|
||||||
if (!this.shouldShowCamions) return [];
|
|
||||||
const h = hour === '' ? 0 : parseInt(hour);
|
|
||||||
const showsInSlot = this.shows().filter(show => {
|
const showsInSlot = this.shows().filter(show => {
|
||||||
if (!show.date) return false;
|
if (!show.date) return false;
|
||||||
const d = new Date(show.date);
|
const d = new Date(show.date);
|
||||||
@@ -282,10 +342,12 @@ export class Planning implements OnInit, AfterViewInit {
|
|||||||
return this.trucks().filter(t => t.showId === showId);
|
return this.trucks().filter(t => t.showId === showId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Retourne uniquement les camions dont le statut calculé est "Disponible"
|
||||||
getAvailableTrucks(): PyroFetesDTOTruckResponseReadTruckDto[] {
|
getAvailableTrucks(): PyroFetesDTOTruckResponseReadTruckDto[] {
|
||||||
return this.trucks().filter(t => !t.showId);
|
return this.trucks().filter(t => this.getComputedTruckStatus(t) === 'Disponible');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Actions camions ──────────────────────────────────────────
|
||||||
async assignTruck(truckId: number, showId: number): Promise<void> {
|
async assignTruck(truckId: number, showId: number): Promise<void> {
|
||||||
await firstValueFrom(this.trucksService.pyroFetesEndpointsTruckUpdateTruckEndpoint(
|
await firstValueFrom(this.trucksService.pyroFetesEndpointsTruckUpdateTruckEndpoint(
|
||||||
truckId, { showId }
|
truckId, { showId }
|
||||||
@@ -371,8 +433,8 @@ export class Planning implements OnInit, AfterViewInit {
|
|||||||
return `${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`;
|
return `${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
isCamionFilterActive = false;
|
isCamionFilterActive = true;
|
||||||
isShowFilterActive = false;
|
isShowFilterActive = true;
|
||||||
|
|
||||||
camionFilter(): void {
|
camionFilter(): void {
|
||||||
this.isCamionFilterActive = !this.isCamionFilterActive;
|
this.isCamionFilterActive = !this.isCamionFilterActive;
|
||||||
|
|||||||
Reference in New Issue
Block a user