2026-03-24 14:19:49 +03:00

725 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>