diff --git a/frontend/components/CalendarGrid.vue b/frontend/components/CalendarGrid.vue
index 3a82993..da090a7 100644
--- a/frontend/components/CalendarGrid.vue
+++ b/frontend/components/CalendarGrid.vue
@@ -30,12 +30,12 @@
@@ -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')
diff --git a/frontend/pages/sites.vue b/frontend/pages/sites.vue
index f1d9510..7614029 100644
--- a/frontend/pages/sites.vue
+++ b/frontend/pages/sites.vue
@@ -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)"
>
-
{{ site.name }}
+
+ ::
+ {{ site.name }}
+
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([])
const editingSite = ref(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
+ }
+}
diff --git a/frontend/services/dto/site.ts b/frontend/services/dto/site.ts
index 037ac1a..02eb6d6 100644
--- a/frontend/services/dto/site.ts
+++ b/frontend/services/dto/site.ts
@@ -2,4 +2,5 @@ export type Site = {
id: number
name: string
color: string
+ displayOrder?: number
}
diff --git a/frontend/services/sites.ts b/frontend/services/sites.ts
index 50e87dd..2eea19a 100644
--- a/frontend/services/sites.ts
+++ b/frontend/services/sites.ts
@@ -8,10 +8,15 @@ export const listSites = async () => {
{},
{ toast: false }
)
- return extractItems(data)
+ return extractItems(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) => {
+export const createSite = async (payload: Pick & { displayOrder?: number }) => {
const api = useApi()
return api.post('/sites', payload, {
toastSuccessKey: 'success.site.create',
@@ -19,7 +24,10 @@ export const createSite = async (payload: Pick) => {
})
}
-export const updateSite = async (id: number, payload: Pick) => {
+export const updateSite = async (
+ id: number,
+ payload: Pick & { displayOrder?: number }
+) => {
const api = useApi()
return api.patch(`/sites/${id}`, payload, {
toastSuccessKey: 'success.site.update',
@@ -27,6 +35,15 @@ export const updateSite = async (id: number, payload: Pick {
+ const api = useApi()
+ return api.patch(`/sites/${id}`, {
+ displayOrder
+ }, {
+ toast: false
+ })
+}
+
export const deleteSite = async (id: number) => {
const api = useApi()
return api.delete(`/sites/${id}`, {}, {
diff --git a/migrations/Version20260216143000.php b/migrations/Version20260216143000.php
new file mode 100644
index 0000000..8cc4263
--- /dev/null
+++ b/migrations/Version20260216143000.php
@@ -0,0 +1,26 @@
+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');
+ }
+}
diff --git a/migrations/Version20260216143100.php b/migrations/Version20260216143100.php
new file mode 100644
index 0000000..499fb09
--- /dev/null
+++ b/migrations/Version20260216143100.php
@@ -0,0 +1,37 @@
+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.
+ }
+}
diff --git a/src/Entity/Site.php b/src/Entity/Site.php
index 6dff18a..554a9c9 100644
--- a/src/Entity/Site.php
+++ b/src/Entity/Site.php
@@ -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;
+ }
}
diff --git a/src/State/AbsencePrintProvider.php b/src/State/AbsencePrintProvider.php
index 8e6329c..e46c02e 100644
--- a/src/State/AbsencePrintProvider.php
+++ b/src/State/AbsencePrintProvider.php
@@ -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')
diff --git a/templates/absence/print.html.twig b/templates/absence/print.html.twig
index 9dd487a..4c4b88a 100644
--- a/templates/absence/print.html.twig
+++ b/templates/absence/print.html.twig
@@ -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;