[#328] Corrections #3
@@ -30,12 +30,12 @@
|
||||
<div
|
||||
v-for="day in daysInMonth"
|
||||
:key="employee.id + '-' + day.date"
|
||||
class="border-b border-neutral-100 px-2 py-2 text-center text-xs text-neutral-800"
|
||||
class="border-b border-neutral-300 px-2 py-2 text-center text-xs text-neutral-800 hover:bg-neutral-500"
|
||||
>
|
||||
<template v-if="getCellInfo(employee.id, day.date)">
|
||||
<button
|
||||
type="button"
|
||||
class="relative flex h-8 w-full items-center justify-center overflow-hidden rounded-md border border-neutral-200 text-[11px] font-semibold text-neutral-800 hover:border-primary-500/40"
|
||||
class="relative flex h-8 w-full items-center justify-center overflow-hidden rounded-md border border-neutral-200 text-[11px] font-semibold text-neutral-800 hover:border-white"
|
||||
:class="isHolidayDate(day.date) ? 'cursor-not-allowed opacity-80' : ''"
|
||||
:style="getCellStyle(employee.id, day.date)"
|
||||
:disabled="isHolidayDate(day.date)"
|
||||
@@ -63,7 +63,7 @@
|
||||
<template v-else>
|
||||
<button
|
||||
type="button"
|
||||
class="relative flex h-8 w-full items-center justify-center rounded-md border border-neutral-200 text-[11px] font-semibold text-neutral-800 hover:border-primary-500/40"
|
||||
class="relative flex h-8 w-full items-center justify-center rounded-md border border-neutral-200 text-[11px] font-semibold text-neutral-800 bg-white hover:border-white"
|
||||
:class="isHolidayDate(day.date) ? 'cursor-not-allowed opacity-80' : ''"
|
||||
:style="getCellStyle(employee.id, day.date)"
|
||||
:disabled="isHolidayDate(day.date)"
|
||||
|
||||
@@ -35,13 +35,6 @@
|
||||
>
|
||||
Sites
|
||||
</NuxtLink>
|
||||
<NuxtLink
|
||||
to="/users"
|
||||
class="flex items-center gap-3 px-4 py-3 text-md font-semibold text-neutral-700 hover:bg-primary-50 hover:text-primary-600"
|
||||
active-class="bg-primary-50 text-primary-600"
|
||||
>
|
||||
Utilisateurs
|
||||
</NuxtLink>
|
||||
<NuxtLink
|
||||
to="/absence-types"
|
||||
class="flex items-center gap-3 px-4 py-3 text-md font-semibold text-neutral-700 hover:bg-primary-50 hover:text-primary-600"
|
||||
@@ -49,6 +42,13 @@
|
||||
>
|
||||
Types d'absence
|
||||
</NuxtLink>
|
||||
<NuxtLink
|
||||
to="/users"
|
||||
class="flex items-center gap-3 px-4 py-3 text-md font-semibold text-neutral-700 hover:bg-primary-50 hover:text-primary-600"
|
||||
active-class="bg-primary-50 text-primary-600"
|
||||
>
|
||||
Utilisateurs
|
||||
</NuxtLink>
|
||||
</template>
|
||||
</nav>
|
||||
|
||||
|
||||
@@ -53,14 +53,24 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 flex items-center gap-4">
|
||||
<input
|
||||
v-model="employeeFilter"
|
||||
type="text"
|
||||
placeholder="Chercher un employé (nom ou prénom)"
|
||||
class="h-10 w-full max-w-md rounded-md border border-neutral-300 bg-white px-3 text-md text-neutral-900"
|
||||
/>
|
||||
<div class="flex justify-between mt-3">
|
||||
<div class="flex items-center gap-4 w-80">
|
||||
<input
|
||||
v-model="employeeFilter"
|
||||
type="text"
|
||||
placeholder="Chercher un employé (nom ou prénom)"
|
||||
class="h-10 w-full max-w-md rounded-md border border-neutral-300 bg-white px-3 text-md text-neutral-900"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-4 rounded-md border border-neutral-300 px-3 py-2">
|
||||
<p class="font-bold">Légende :</p>
|
||||
<div v-for="type in absenceTypes" :key="type.id" class="flex items-center gap-2">
|
||||
<div :style="{ backgroundColor: type.color }" class="h-4 w-4 rounded"></div>
|
||||
<p>{{ type.label }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-h-0">
|
||||
@@ -123,7 +133,12 @@ const sites = computed(() => {
|
||||
siteMap.set(employee.site.id, employee.site)
|
||||
}
|
||||
}
|
||||
return Array.from(siteMap.values()).sort((siteA, siteB) => siteA.name.localeCompare(siteB.name, 'fr'))
|
||||
return Array.from(siteMap.values()).sort((siteA, siteB) => {
|
||||
const orderA = siteA.displayOrder ?? 0
|
||||
const orderB = siteB.displayOrder ?? 0
|
||||
if (orderA !== orderB) return orderA - orderB
|
||||
return siteA.name.localeCompare(siteB.name, 'fr')
|
||||
})
|
||||
})
|
||||
|
||||
// Filtres de sites (par défaut: tous sélectionnés à l'init).
|
||||
@@ -139,6 +154,9 @@ watch(sites, (next) => {
|
||||
// Tri stable: site -> nom -> prénom.
|
||||
const sortedEmployees = computed(() => {
|
||||
return [...employees.value].sort((employeeA, employeeB) => {
|
||||
const siteOrderA = employeeA.site?.displayOrder ?? 0
|
||||
const siteOrderB = employeeB.site?.displayOrder ?? 0
|
||||
if (siteOrderA !== siteOrderB) return siteOrderA - siteOrderB
|
||||
const siteNameA = employeeA.site?.name ?? ''
|
||||
const siteNameB = employeeB.site?.name ?? ''
|
||||
if (siteNameA !== siteNameB) return siteNameA.localeCompare(siteNameB, 'fr')
|
||||
|
||||
@@ -32,8 +32,15 @@
|
||||
v-for="site in sites"
|
||||
:key="site.id"
|
||||
class="grid grid-cols-[1fr_140px_160px] items-center gap-4 border-b border-neutral-100 px-6 py-3 text-md text-neutral-800 last:border-b-0"
|
||||
draggable="true"
|
||||
@dragstart="handleDragStart($event, site)"
|
||||
@dragover="handleDragOver"
|
||||
@drop="handleDrop($event, site)"
|
||||
>
|
||||
<span class="text-left">{{ site.name }}</span>
|
||||
<span class="flex items-center gap-2 text-left cursor-pointer">
|
||||
<span class="select-none text-xs">::</span>
|
||||
<span>{{ site.name }}</span>
|
||||
</span>
|
||||
<div class="flex items-center gap-2 justify-start">
|
||||
<span
|
||||
class="inline-block h-3 w-3 rounded-full"
|
||||
@@ -114,11 +121,12 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Site } from '~/services/dto/site'
|
||||
import { createSite, deleteSite, listSites, updateSite } from '~/services/sites'
|
||||
import { createSite, deleteSite, listSites, updateSite, updateSiteOrder } from '~/services/sites'
|
||||
|
||||
const isDrawerOpen = ref(false)
|
||||
const isSubmitting = ref(false)
|
||||
const isLoading = ref(false)
|
||||
const isReordering = ref(false)
|
||||
|
||||
const sites = ref<Site[]>([])
|
||||
const editingSite = ref<Site | null>(null)
|
||||
@@ -207,7 +215,8 @@ const handleSubmit = async () => {
|
||||
} else {
|
||||
await createSite({
|
||||
name: form.name,
|
||||
color: form.color
|
||||
color: form.color,
|
||||
displayOrder: sites.value.length + 1
|
||||
})
|
||||
}
|
||||
|
||||
@@ -231,4 +240,52 @@ const confirmDelete = async (site: Site) => {
|
||||
await deleteSite(site.id)
|
||||
await loadSites()
|
||||
}
|
||||
|
||||
const handleDragStart = (event: DragEvent, site: Site) => {
|
||||
if (isReordering.value || !event.dataTransfer) return
|
||||
event.dataTransfer.effectAllowed = 'move'
|
||||
event.dataTransfer.setData('text/plain', String(site.id))
|
||||
}
|
||||
|
||||
const handleDragOver = (event: DragEvent) => {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
const handleDrop = async (event: DragEvent, site: Site) => {
|
||||
event.preventDefault()
|
||||
if (isReordering.value) return
|
||||
|
||||
const dragId = Number(event.dataTransfer?.getData('text/plain'))
|
||||
if (!dragId || dragId === site.id) return
|
||||
|
||||
const fromIndex = sites.value.findIndex((item) => item.id === dragId)
|
||||
const toIndex = sites.value.findIndex((item) => item.id === site.id)
|
||||
if (fromIndex < 0 || toIndex < 0 || fromIndex === toIndex) return
|
||||
|
||||
const reordered = [...sites.value]
|
||||
const [moved] = reordered.splice(fromIndex, 1)
|
||||
reordered.splice(toIndex, 0, moved)
|
||||
|
||||
const updates: Array<{ id: number; displayOrder: number }> = []
|
||||
reordered.forEach((item, index) => {
|
||||
const nextOrder = index + 1
|
||||
if ((item.displayOrder ?? 0) !== nextOrder) {
|
||||
updates.push({ id: item.id, displayOrder: nextOrder })
|
||||
}
|
||||
item.displayOrder = nextOrder
|
||||
})
|
||||
|
||||
sites.value = reordered
|
||||
if (updates.length === 0) return
|
||||
|
||||
isReordering.value = true
|
||||
try {
|
||||
await Promise.all(updates.map((update) => updateSiteOrder(update.id, update.displayOrder)))
|
||||
} catch {
|
||||
window.alert("Impossible de reordonner les sites.")
|
||||
await loadSites()
|
||||
} finally {
|
||||
isReordering.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -2,4 +2,5 @@ export type Site = {
|
||||
id: number
|
||||
name: string
|
||||
color: string
|
||||
displayOrder?: number
|
||||
}
|
||||
|
||||
@@ -8,10 +8,15 @@ export const listSites = async () => {
|
||||
{},
|
||||
{ toast: false }
|
||||
)
|
||||
return extractItems<Site>(data)
|
||||
return extractItems<Site>(data).sort((siteA, siteB) => {
|
||||
const orderA = siteA.displayOrder ?? 0
|
||||
const orderB = siteB.displayOrder ?? 0
|
||||
if (orderA !== orderB) return orderA - orderB
|
||||
return siteA.name.localeCompare(siteB.name, 'fr')
|
||||
})
|
||||
}
|
||||
|
||||
export const createSite = async (payload: Pick<Site, 'name' | 'color'>) => {
|
||||
export const createSite = async (payload: Pick<Site, 'name' | 'color'> & { displayOrder?: number }) => {
|
||||
const api = useApi()
|
||||
return api.post<Site>('/sites', payload, {
|
||||
toastSuccessKey: 'success.site.create',
|
||||
@@ -19,7 +24,10 @@ export const createSite = async (payload: Pick<Site, 'name' | 'color'>) => {
|
||||
})
|
||||
}
|
||||
|
||||
export const updateSite = async (id: number, payload: Pick<Site, 'name' | 'color'>) => {
|
||||
export const updateSite = async (
|
||||
id: number,
|
||||
payload: Pick<Site, 'name' | 'color'> & { displayOrder?: number }
|
||||
) => {
|
||||
const api = useApi()
|
||||
return api.patch<Site>(`/sites/${id}`, payload, {
|
||||
toastSuccessKey: 'success.site.update',
|
||||
@@ -27,6 +35,15 @@ export const updateSite = async (id: number, payload: Pick<Site, 'name' | 'color
|
||||
})
|
||||
}
|
||||
|
||||
export const updateSiteOrder = async (id: number, displayOrder: number) => {
|
||||
const api = useApi()
|
||||
return api.patch<Site>(`/sites/${id}`, {
|
||||
displayOrder
|
||||
}, {
|
||||
toast: false
|
||||
})
|
||||
}
|
||||
|
||||
export const deleteSite = async (id: number) => {
|
||||
const api = useApi()
|
||||
return api.delete(`/sites/${id}`, {}, {
|
||||
|
||||
26
migrations/Version20260216143000.php
Normal file
26
migrations/Version20260216143000.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20260216143000 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add display_order to sites';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE sites ADD display_order INT NOT NULL DEFAULT 0');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE sites DROP COLUMN display_order');
|
||||
}
|
||||
}
|
||||
37
migrations/Version20260216143100.php
Normal file
37
migrations/Version20260216143100.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20260216143100 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Backfill site display_order';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('
|
||||
UPDATE sites s
|
||||
SET display_order = ranked.rn
|
||||
FROM (
|
||||
SELECT id,
|
||||
ROW_NUMBER() OVER (
|
||||
ORDER BY name ASC, id ASC
|
||||
) AS rn
|
||||
FROM sites
|
||||
) ranked
|
||||
WHERE ranked.id = s.id
|
||||
');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// Pas de rollback pertinent.
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,10 @@ class Site
|
||||
#[Groups(['site:read', 'employee:read'])]
|
||||
private string $color = '';
|
||||
|
||||
#[ORM\Column(type: 'integer', options: ['default' => 0])]
|
||||
#[Groups(['site:read'])]
|
||||
private int $displayOrder = 0;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
@@ -59,4 +63,16 @@ class Site
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDisplayOrder(): int
|
||||
{
|
||||
return $this->displayOrder;
|
||||
}
|
||||
|
||||
public function setDisplayOrder(int $displayOrder): self
|
||||
{
|
||||
$this->displayOrder = $displayOrder;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,7 +114,8 @@ class AbsencePrintProvider implements ProviderInterface
|
||||
->createQueryBuilder('e')
|
||||
->leftJoin('e.site', 's')
|
||||
->addSelect('s')
|
||||
->orderBy('s.name', 'ASC')
|
||||
->orderBy('s.displayOrder', 'ASC')
|
||||
->addOrderBy('s.name', 'ASC')
|
||||
->addOrderBy('e.displayOrder', 'ASC')
|
||||
->addOrderBy('e.lastName', 'ASC')
|
||||
->addOrderBy('e.firstName', 'ASC')
|
||||
|
||||
@@ -35,29 +35,27 @@
|
||||
}
|
||||
|
||||
.col-employee {
|
||||
font-size: 16px;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
width: 10mm;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.col-day {
|
||||
font-size: 10px;
|
||||
font-size: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.month {
|
||||
font-size: 16px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
padding: 3px 0;
|
||||
}
|
||||
|
||||
.site-title td {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
border-color: #0a0a0a;
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
.site-title .label {
|
||||
@@ -66,7 +64,7 @@
|
||||
|
||||
.code {
|
||||
font-weight: bold;
|
||||
font-size: 9px;
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.holiday-code {
|
||||
@@ -95,8 +93,6 @@
|
||||
|
||||
.full-cell {
|
||||
display: block;
|
||||
height: 6mm;
|
||||
line-height: 6mm;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -104,7 +100,6 @@
|
||||
|
||||
.half-table {
|
||||
width: 100%;
|
||||
height: 6mm;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
border: 0;
|
||||
|
||||
Reference in New Issue
Block a user