725 lines
16 KiB
Vue
725 lines
16 KiB
Vue
<template>
|
||
<div class="planning-kanban-root">
|
||
<!-- Toolbar -->
|
||
<div class="kanban-toolbar">
|
||
<div class="toolbar-left">
|
||
<div v-for="col in columnsConfig" :key="col.id" class="toolbar-stat">
|
||
<span class="stat-dot" :style="{ background: col.color }"></span>
|
||
<span class="stat-label">{{ col.title }}</span>
|
||
<span class="stat-count">{{ countByStatus(col.status) }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="toolbar-right">
|
||
<span class="total-badge">
|
||
<i class="fas fa-list-ul me-1"></i>
|
||
{{ interventions.length }} intervention{{
|
||
interventions.length > 1 ? "s" : ""
|
||
}}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Kanban Board -->
|
||
<div class="kanban-scroll-area">
|
||
<div class="kanban-columns">
|
||
<div
|
||
v-for="col in columnsConfig"
|
||
:key="col.id"
|
||
class="kanban-column"
|
||
:data-col-id="col.id"
|
||
>
|
||
<!-- Column Header -->
|
||
<div class="col-header" :style="{ '--col-color': col.color }">
|
||
<div class="col-header-top">
|
||
<div class="col-title-row">
|
||
<span class="col-dot"></span>
|
||
<span class="col-title">{{ col.title }}</span>
|
||
</div>
|
||
<span class="col-badge">{{ countByStatus(col.status) }}</span>
|
||
</div>
|
||
<div class="col-progress-bar">
|
||
<div
|
||
class="col-progress-fill"
|
||
:style="{ width: progressWidth(col.status) + '%' }"
|
||
></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Cards Drop Zone -->
|
||
<div
|
||
class="cards-zone"
|
||
:data-status="col.status"
|
||
@dragover.prevent="onDragOver($event, col.id)"
|
||
@dragleave="onDragLeave($event)"
|
||
@drop="onDrop($event, col)"
|
||
>
|
||
<transition-group name="card-list" tag="div">
|
||
<div
|
||
v-for="item in itemsByStatus(col.status)"
|
||
:key="item.id"
|
||
class="kanban-card"
|
||
:class="{ 'is-dragging': draggingId === item.id.toString() }"
|
||
:style="{ '--type-color': getTypeColor(item.type) }"
|
||
draggable="true"
|
||
@dragstart="onDragStart($event, item)"
|
||
@dragend="onDragEnd"
|
||
@click="emit('edit', item)"
|
||
>
|
||
<!-- Left accent strip -->
|
||
<div class="card-strip"></div>
|
||
|
||
<!-- Card Body -->
|
||
<div class="card-inner">
|
||
<!-- Top row: type badge + time -->
|
||
<div class="card-top">
|
||
<span class="type-badge">
|
||
<i :class="getTypeIcon(item.type)" class="type-icon"></i>
|
||
{{ item.type || "Soin" }}
|
||
</span>
|
||
<span class="card-time">
|
||
<i class="far fa-clock time-icon"></i>
|
||
{{ formatTime(item.date) }}
|
||
</span>
|
||
</div>
|
||
|
||
<!-- Deceased name -->
|
||
<div class="card-deceased">
|
||
{{ item.deceased || "Non spécifié" }}
|
||
</div>
|
||
|
||
<!-- Client -->
|
||
<div class="card-client">
|
||
<i class="fas fa-user card-client-icon"></i>
|
||
<span>{{ item.client || "–" }}</span>
|
||
</div>
|
||
|
||
<!-- Bottom row: date + collaborator -->
|
||
<div class="card-footer-row">
|
||
<div class="card-date">
|
||
<i class="far fa-calendar-alt"></i>
|
||
<span>{{ formatDate(item.date) }}</span>
|
||
</div>
|
||
<div class="collab-avatar" :title="item.collaborator">
|
||
{{ getInitials(item.collaborator) }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Hover action bar -->
|
||
<div class="card-hover-bar">
|
||
<button
|
||
class="hover-action"
|
||
title="Modifier"
|
||
@click.stop="emit('edit', item)"
|
||
>
|
||
<i class="fas fa-pen"></i>
|
||
</button>
|
||
<div class="hover-divider"></div>
|
||
<span class="hover-hint">Glisser pour déplacer</span>
|
||
</div>
|
||
</div>
|
||
</transition-group>
|
||
|
||
<!-- Empty state -->
|
||
<div
|
||
v-if="itemsByStatus(col.status).length === 0"
|
||
class="empty-column"
|
||
>
|
||
<i class="fas fa-inbox empty-icon"></i>
|
||
<span>Aucune intervention</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, defineProps, defineEmits } from "vue";
|
||
|
||
const props = defineProps({
|
||
interventions: {
|
||
type: Array,
|
||
default: () => [],
|
||
},
|
||
});
|
||
|
||
const emit = defineEmits(["edit", "update-status"]);
|
||
|
||
const draggingId = ref(null);
|
||
const draggingItem = ref(null);
|
||
const dragOverCol = ref(null);
|
||
|
||
const columnsConfig = [
|
||
{
|
||
id: "todo",
|
||
title: "À planifier",
|
||
status: "En attente",
|
||
color: "#f59e0b",
|
||
icon: "fas fa-hourglass-half",
|
||
},
|
||
{
|
||
id: "planned",
|
||
title: "Confirmé",
|
||
status: "Confirmé",
|
||
color: "#3b82f6",
|
||
icon: "fas fa-calendar-check",
|
||
},
|
||
{
|
||
id: "in-progress",
|
||
title: "En cours",
|
||
status: "En cours",
|
||
color: "#8b5cf6",
|
||
icon: "fas fa-bolt",
|
||
},
|
||
{
|
||
id: "done",
|
||
title: "Terminé",
|
||
status: "Terminé",
|
||
color: "#10b981",
|
||
icon: "fas fa-check-circle",
|
||
},
|
||
];
|
||
|
||
const typeColors = {
|
||
Soin: "#3b82f6",
|
||
Transport: "#10b981",
|
||
"Mise en bière": "#f59e0b",
|
||
Cérémonie: "#8b5cf6",
|
||
};
|
||
|
||
const typeIcons = {
|
||
Soin: "fas fa-heartbeat",
|
||
Transport: "fas fa-car",
|
||
"Mise en bière": "fas fa-box",
|
||
Cérémonie: "fas fa-dove",
|
||
};
|
||
|
||
const getTypeColor = (type) => typeColors[type] || "#6b7280";
|
||
const getTypeIcon = (type) => typeIcons[type] || "fas fa-briefcase-medical";
|
||
|
||
const formatTime = (d) => {
|
||
try {
|
||
return new Date(d).toLocaleTimeString("fr-FR", {
|
||
hour: "2-digit",
|
||
minute: "2-digit",
|
||
});
|
||
} catch {
|
||
return "--:--";
|
||
}
|
||
};
|
||
const formatDate = (d) => {
|
||
try {
|
||
return new Date(d).toLocaleDateString("fr-FR", {
|
||
day: "numeric",
|
||
month: "short",
|
||
});
|
||
} catch {
|
||
return "–";
|
||
}
|
||
};
|
||
const getInitials = (n) =>
|
||
n
|
||
? n
|
||
.split(" ")
|
||
.map((s) => s[0])
|
||
.join("")
|
||
.substring(0, 2)
|
||
.toUpperCase()
|
||
: "?";
|
||
|
||
const itemsByStatus = (status) =>
|
||
props.interventions.filter((i) => i.status === status);
|
||
const countByStatus = (status) => itemsByStatus(status).length;
|
||
const progressWidth = (status) => {
|
||
if (!props.interventions.length) return 0;
|
||
return Math.round((countByStatus(status) / props.interventions.length) * 100);
|
||
};
|
||
|
||
// ─── Native Drag & Drop ───
|
||
const onDragStart = (e, item) => {
|
||
draggingId.value = item.id.toString();
|
||
draggingItem.value = item;
|
||
e.dataTransfer.effectAllowed = "move";
|
||
e.dataTransfer.setData("text/plain", item.id.toString());
|
||
};
|
||
|
||
const onDragEnd = () => {
|
||
draggingId.value = null;
|
||
draggingItem.value = null;
|
||
dragOverCol.value = null;
|
||
document
|
||
.querySelectorAll(".cards-zone")
|
||
.forEach((z) => z.classList.remove("drag-over"));
|
||
};
|
||
|
||
const onDragOver = (e, colId) => {
|
||
e.preventDefault();
|
||
e.dataTransfer.dropEffect = "move";
|
||
if (dragOverCol.value !== colId) {
|
||
document
|
||
.querySelectorAll(".cards-zone")
|
||
.forEach((z) => z.classList.remove("drag-over"));
|
||
dragOverCol.value = colId;
|
||
e.currentTarget.classList.add("drag-over");
|
||
}
|
||
};
|
||
|
||
const onDragLeave = (e) => {
|
||
if (!e.currentTarget.contains(e.relatedTarget)) {
|
||
e.currentTarget.classList.remove("drag-over");
|
||
}
|
||
};
|
||
|
||
const onDrop = (e, col) => {
|
||
e.preventDefault();
|
||
e.currentTarget.classList.remove("drag-over");
|
||
const id = e.dataTransfer.getData("text/plain");
|
||
if (id && draggingItem.value) {
|
||
emit("update-status", { id, status: col.status });
|
||
}
|
||
draggingId.value = null;
|
||
draggingItem.value = null;
|
||
dragOverCol.value = null;
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* ─── Root ─── */
|
||
.planning-kanban-root {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: calc(100vh - 180px);
|
||
min-height: 500px;
|
||
}
|
||
|
||
/* ─── Toolbar ─── */
|
||
.kanban-toolbar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 10px 4px 14px;
|
||
flex-shrink: 0;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
}
|
||
|
||
.toolbar-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.toolbar-stat {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
background: #fff;
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 20px;
|
||
padding: 4px 10px 4px 8px;
|
||
font-size: 0.76rem;
|
||
}
|
||
|
||
.stat-dot {
|
||
width: 7px;
|
||
height: 7px;
|
||
border-radius: 50%;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.stat-label {
|
||
color: #6b7280;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.stat-count {
|
||
font-weight: 700;
|
||
color: #111827;
|
||
margin-left: 2px;
|
||
}
|
||
|
||
.total-badge {
|
||
font-size: 0.78rem;
|
||
font-weight: 600;
|
||
color: #374151;
|
||
background: #f3f4f6;
|
||
border-radius: 20px;
|
||
padding: 5px 12px;
|
||
}
|
||
|
||
/* ─── Scroll Area ─── */
|
||
.kanban-scroll-area {
|
||
flex: 1;
|
||
overflow-x: auto;
|
||
overflow-y: hidden;
|
||
padding-bottom: 8px;
|
||
}
|
||
|
||
.kanban-scroll-area::-webkit-scrollbar {
|
||
height: 5px;
|
||
}
|
||
.kanban-scroll-area::-webkit-scrollbar-track {
|
||
background: #f1f5f9;
|
||
border-radius: 10px;
|
||
}
|
||
.kanban-scroll-area::-webkit-scrollbar-thumb {
|
||
background: #cbd5e1;
|
||
border-radius: 10px;
|
||
}
|
||
.kanban-scroll-area::-webkit-scrollbar-thumb:hover {
|
||
background: #94a3b8;
|
||
}
|
||
|
||
/* ─── Columns ─── */
|
||
.kanban-columns {
|
||
display: flex;
|
||
gap: 14px;
|
||
height: 100%;
|
||
min-width: max-content;
|
||
padding: 2px 2px 6px;
|
||
}
|
||
|
||
.kanban-column {
|
||
width: 300px;
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
}
|
||
|
||
/* ─── Column Header ─── */
|
||
.col-header {
|
||
background: #fff;
|
||
border-radius: 12px 12px 0 0;
|
||
border: 1px solid #e5e7eb;
|
||
border-bottom: none;
|
||
padding: 13px 14px 10px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.col-header-top {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.col-title-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.col-dot {
|
||
width: 9px;
|
||
height: 9px;
|
||
border-radius: 50%;
|
||
background: var(--col-color);
|
||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--col-color) 20%, transparent);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.col-title {
|
||
font-size: 0.82rem;
|
||
font-weight: 700;
|
||
color: #1f2937;
|
||
letter-spacing: 0.01em;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.col-badge {
|
||
min-width: 22px;
|
||
height: 22px;
|
||
border-radius: 6px;
|
||
background: color-mix(in srgb, var(--col-color) 12%, transparent);
|
||
color: var(--col-color);
|
||
font-size: 0.72rem;
|
||
font-weight: 800;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 0 5px;
|
||
}
|
||
|
||
.col-progress-bar {
|
||
height: 3px;
|
||
background: #f1f5f9;
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.col-progress-fill {
|
||
height: 100%;
|
||
background: var(--col-color);
|
||
border-radius: 4px;
|
||
transition: width 0.5s ease;
|
||
}
|
||
|
||
/* ─── Cards Zone ─── */
|
||
.cards-zone {
|
||
flex: 1;
|
||
background: #f8fafc;
|
||
border: 1px solid #e5e7eb;
|
||
border-top: none;
|
||
border-radius: 0 0 12px 12px;
|
||
padding: 10px;
|
||
overflow-y: auto;
|
||
transition: background 0.2s;
|
||
min-height: 100px;
|
||
}
|
||
|
||
.cards-zone::-webkit-scrollbar {
|
||
width: 4px;
|
||
}
|
||
.cards-zone::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
}
|
||
.cards-zone::-webkit-scrollbar-thumb {
|
||
background: #e2e8f0;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.cards-zone.drag-over {
|
||
background: color-mix(in srgb, #3b82f6 6%, #f8fafc);
|
||
border-color: #93c5fd;
|
||
box-shadow: inset 0 0 0 2px #bfdbfe;
|
||
}
|
||
|
||
/* ─── Card ─── */
|
||
.kanban-card {
|
||
position: relative;
|
||
background: #fff;
|
||
border-radius: 10px;
|
||
margin-bottom: 9px;
|
||
border: 1px solid #e9edf2;
|
||
overflow: hidden;
|
||
cursor: grab;
|
||
transition: transform 0.18s ease, box-shadow 0.18s ease, opacity 0.18s;
|
||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
|
||
}
|
||
|
||
.kanban-card:hover {
|
||
transform: translateY(-3px);
|
||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
|
||
border-color: #dde3ea;
|
||
}
|
||
|
||
.kanban-card:active,
|
||
.kanban-card.is-dragging {
|
||
opacity: 0.5;
|
||
cursor: grabbing;
|
||
transform: scale(0.97);
|
||
}
|
||
|
||
.kanban-card:hover .card-hover-bar {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
|
||
/* Left accent strip */
|
||
.card-strip {
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
bottom: 0;
|
||
width: 4px;
|
||
background: var(--type-color);
|
||
border-radius: 4px 0 0 4px;
|
||
}
|
||
|
||
/* ─── Card Inner ─── */
|
||
.card-inner {
|
||
padding: 11px 12px 11px 16px;
|
||
}
|
||
|
||
.card-top {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.type-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
font-size: 0.7rem;
|
||
font-weight: 600;
|
||
color: var(--type-color);
|
||
background: color-mix(in srgb, var(--type-color) 10%, transparent);
|
||
border: 1px solid color-mix(in srgb, var(--type-color) 20%, transparent);
|
||
border-radius: 5px;
|
||
padding: 2px 7px;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.04em;
|
||
}
|
||
|
||
.type-icon {
|
||
font-size: 0.65rem;
|
||
}
|
||
|
||
.card-time {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
font-size: 0.73rem;
|
||
font-weight: 600;
|
||
color: #374151;
|
||
}
|
||
|
||
.time-icon {
|
||
color: #9ca3af;
|
||
font-size: 0.68rem;
|
||
}
|
||
|
||
.card-deceased {
|
||
font-size: 0.88rem;
|
||
font-weight: 700;
|
||
color: #111827;
|
||
margin-bottom: 4px;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
letter-spacing: -0.01em;
|
||
}
|
||
|
||
.card-client {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
font-size: 0.74rem;
|
||
color: #6b7280;
|
||
margin-bottom: 10px;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.card-client-icon {
|
||
font-size: 0.65rem;
|
||
color: #9ca3af;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.card-footer-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding-top: 8px;
|
||
border-top: 1px solid #f1f5f9;
|
||
}
|
||
|
||
.card-date {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
font-size: 0.73rem;
|
||
color: #9ca3af;
|
||
}
|
||
|
||
.card-date i {
|
||
font-size: 0.7rem;
|
||
}
|
||
|
||
.collab-avatar {
|
||
width: 26px;
|
||
height: 26px;
|
||
border-radius: 8px;
|
||
background: linear-gradient(135deg, #1a2e4a, #2d4a6e);
|
||
color: #fff;
|
||
font-size: 0.62rem;
|
||
font-weight: 700;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
letter-spacing: 0.03em;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* ─── Hover Action Bar ─── */
|
||
.card-hover-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 6px 12px 6px 16px;
|
||
background: #f8fafc;
|
||
border-top: 1px solid #f1f5f9;
|
||
opacity: 0;
|
||
transform: translateY(4px);
|
||
transition: opacity 0.18s ease, transform 0.18s ease;
|
||
}
|
||
|
||
.hover-action {
|
||
background: none;
|
||
border: none;
|
||
padding: 3px 8px;
|
||
border-radius: 5px;
|
||
font-size: 0.72rem;
|
||
color: #2d4a6e;
|
||
cursor: pointer;
|
||
transition: background 0.15s;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.hover-action:hover {
|
||
background: #e8eef7;
|
||
}
|
||
|
||
.hover-divider {
|
||
width: 1px;
|
||
height: 14px;
|
||
background: #e5e7eb;
|
||
}
|
||
|
||
.hover-hint {
|
||
font-size: 0.68rem;
|
||
color: #9ca3af;
|
||
margin-left: auto;
|
||
}
|
||
|
||
/* ─── Empty State ─── */
|
||
.empty-column {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
padding: 32px 16px;
|
||
color: #cbd5e1;
|
||
text-align: center;
|
||
}
|
||
|
||
.empty-icon {
|
||
font-size: 1.6rem;
|
||
opacity: 0.5;
|
||
}
|
||
|
||
.empty-column span {
|
||
font-size: 0.78rem;
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* ─── Transition animations ─── */
|
||
.card-list-enter-active,
|
||
.card-list-leave-active {
|
||
transition: all 0.28s ease;
|
||
}
|
||
|
||
.card-list-enter-from {
|
||
opacity: 0;
|
||
transform: translateY(-10px) scale(0.97);
|
||
}
|
||
|
||
.card-list-leave-to {
|
||
opacity: 0;
|
||
transform: translateX(20px) scale(0.96);
|
||
}
|
||
|
||
.card-list-move {
|
||
transition: transform 0.28s ease;
|
||
}
|
||
</style>
|