Nouvel style planning
This commit is contained in:
parent
31090d12ba
commit
094c7a0980
@ -0,0 +1,175 @@
|
|||||||
|
<template>
|
||||||
|
<planning-template>
|
||||||
|
<template #header>
|
||||||
|
<div class="d-flex flex-column gap-3">
|
||||||
|
<div>
|
||||||
|
<h1 class="display-4 font-bold bg-gradient-indigo-text mb-1">Planning</h1>
|
||||||
|
<p class="text-sm text-secondary">{{ interventionCount }} intervention(s) (mes tâches)</p>
|
||||||
|
</div>
|
||||||
|
<!-- Date Navigator Mobile Position could go here if needed, but keeping simple for now -->
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-column flex-md-row gap-3 w-100 w-md-auto align-items-center">
|
||||||
|
<!-- Date Navigator -->
|
||||||
|
<planning-date-navigator
|
||||||
|
:current-date="currentDate"
|
||||||
|
@prev-week="$emit('prev-week')"
|
||||||
|
@next-week="$emit('next-week')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<planning-action-button variant="secondary" size="sm" @click="$emit('refresh')">
|
||||||
|
<template #icon><i class="fas fa-sync-alt"></i></template>
|
||||||
|
<span class="d-none d-lg-inline">Actualiser</span>
|
||||||
|
</planning-action-button>
|
||||||
|
<planning-action-button variant="primary" size="sm" @click="$emit('new-request')">
|
||||||
|
<template #icon><i class="fas fa-plus"></i></template>
|
||||||
|
<span class="d-none d-lg-inline">Nouvelle demande</span>
|
||||||
|
<span class="d-lg-none">Nouveau</span>
|
||||||
|
</planning-action-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #view-toggles>
|
||||||
|
<planning-view-toggles v-model:activeView="localActiveView" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #legend>
|
||||||
|
<!-- <planning-legend /> -->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #sidebar>
|
||||||
|
<!-- <planning-collaborators-sidebar :count="collaborators.length">
|
||||||
|
<div v-for="collab in collaborators" :key="collab.id" class="collaborator-item">
|
||||||
|
{{ collab.name }}
|
||||||
|
</div>
|
||||||
|
</planning-collaborators-sidebar> -->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #calendar-grid>
|
||||||
|
<!-- Grille View -->
|
||||||
|
<planning-week-grid
|
||||||
|
v-if="localActiveView === 'grille'"
|
||||||
|
:start-date="currentDate"
|
||||||
|
:interventions="interventions"
|
||||||
|
@cell-click="handleCellClick"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- List View -->
|
||||||
|
<planning-list
|
||||||
|
v-if="localActiveView === 'liste'"
|
||||||
|
:interventions="interventions"
|
||||||
|
@edit="handleEdit"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Kanban View -->
|
||||||
|
<planning-kanban
|
||||||
|
v-if="localActiveView === 'kanban'"
|
||||||
|
:interventions="interventions"
|
||||||
|
@edit="handleEdit"
|
||||||
|
@update-status="handleUpdateStatus"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</planning-template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch, defineProps, defineEmits } from "vue";
|
||||||
|
import PlanningTemplate from "@/components/templates/Planning/PlanningTemplate.vue";
|
||||||
|
import PlanningActionButton from "@/components/atoms/Planning/PlanningActionButton.vue";
|
||||||
|
import PlanningViewToggles from "@/components/molecules/Planning/PlanningViewToggles.vue";
|
||||||
|
import PlanningLegend from "@/components/molecules/Planning/PlanningLegend.vue";
|
||||||
|
import PlanningCollaboratorsSidebar from "@/components/molecules/Planning/PlanningCollaboratorsSidebar.vue";
|
||||||
|
import PlanningWeekGrid from "@/components/molecules/Planning/PlanningWeekGrid.vue";
|
||||||
|
import PlanningList from "@/components/molecules/Planning/PlanningList.vue";
|
||||||
|
import PlanningKanban from "@/components/molecules/Planning/PlanningKanban.vue";
|
||||||
|
import PlanningDateNavigator from "@/components/molecules/Planning/PlanningDateNavigator.vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
interventionCount: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
collaborators: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
interventions: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
currentDate: {
|
||||||
|
type: Date,
|
||||||
|
default: () => new Date()
|
||||||
|
},
|
||||||
|
activeView: {
|
||||||
|
type: String,
|
||||||
|
default: "grille"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits([
|
||||||
|
"refresh",
|
||||||
|
"new-request",
|
||||||
|
"cell-click",
|
||||||
|
"update:activeView",
|
||||||
|
"prev-week",
|
||||||
|
"next-week",
|
||||||
|
"edit-intervention",
|
||||||
|
"update-status"
|
||||||
|
]);
|
||||||
|
|
||||||
|
const localActiveView = ref(props.activeView);
|
||||||
|
|
||||||
|
watch(() => props.activeView, (newVal) => {
|
||||||
|
localActiveView.value = newVal;
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => localActiveView.value, (newVal) => {
|
||||||
|
emit("update:activeView", newVal);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleCellClick = (info) => {
|
||||||
|
emit("cell-click", info);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (intervention) => {
|
||||||
|
emit("edit-intervention", intervention);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateStatus = (payload) => {
|
||||||
|
emit("update-status", payload);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.font-bold {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-gradient-indigo-text {
|
||||||
|
background: linear-gradient(to right, #2563eb, #4f46e5, #9333ea);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-4 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.display-4 {
|
||||||
|
font-size: 1.875rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-secondary {
|
||||||
|
color: #64748b !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-sm {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
<template>
|
||||||
|
<button
|
||||||
|
class="btn d-inline-flex align-items-center justify-content-center gap-2 border-0 shadow-sm transition-all"
|
||||||
|
:class="[
|
||||||
|
variant === 'primary' ? 'btn-primary-gradient' : 'btn-outline-indigo',
|
||||||
|
size === 'sm' ? 'btn-sm py-1 px-3' : 'py-2 px-4'
|
||||||
|
]"
|
||||||
|
@click="$emit('click')"
|
||||||
|
>
|
||||||
|
<slot name="icon"></slot>
|
||||||
|
<span :class="{ 'd-none d-md-inline': hideTextOnMobile }">
|
||||||
|
<slot></slot>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineProps, defineEmits } from "vue";
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
variant: {
|
||||||
|
type: String,
|
||||||
|
default: "secondary" // primary or secondary
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
default: "md"
|
||||||
|
},
|
||||||
|
hideTextOnMobile: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
defineEmits(["click"]);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.btn {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary-gradient {
|
||||||
|
background: linear-gradient(to right, #2563eb, #4f46e5);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary-gradient:hover {
|
||||||
|
background: linear-gradient(to right, #1d4ed8, #4338ca);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-indigo {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #e0e7ff !important;
|
||||||
|
color: #4f46e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-indigo:hover {
|
||||||
|
background-color: #f5f7ff;
|
||||||
|
color: #4338ca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transition-all {
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
<template>
|
||||||
|
<div class="card border-0 shadow-lg rounded-xl h-100 sidebar-card" :class="{ 'collapsed': isCollapsed }">
|
||||||
|
<div class="card-header bg-white border-0 pb-0">
|
||||||
|
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<i class="fas fa-users text-indigo-600"></i>
|
||||||
|
<h3 class="text-xs font-semibold mb-0">👥 Collaborateurs ({{ count }})</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body pt-0">
|
||||||
|
<div v-if="count === 0" class="text-center text-secondary py-4 text-xs">
|
||||||
|
<p class="mb-2">Aucun collaborateur</p>
|
||||||
|
<p class="text-muted opacity-60">💡 Allouez une couleur depuis la page Salariés</p>
|
||||||
|
</div>
|
||||||
|
<slot v-else></slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle bit for mobile if needed, though we handle collapse in template -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineProps } from "vue";
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
count: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
isCollapsed: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.sidebar-card {
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
background-color: white;
|
||||||
|
border: 2px solid #e0e7ff; /* border-indigo-200 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-xs {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-semibold {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-indigo-600 {
|
||||||
|
color: #4f46e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-card {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.sidebar-card.collapsed {
|
||||||
|
width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,93 @@
|
|||||||
|
<template>
|
||||||
|
<div class="d-flex align-items-center bg-white shadow-sm rounded-lg p-1 border">
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-icon mb-0 shadow-none border-0 hover:bg-gray-100 rounded-md"
|
||||||
|
@click="$emit('prev-week')"
|
||||||
|
>
|
||||||
|
<i class="fas fa-chevron-left text-secondary text-xs"></i>
|
||||||
|
</button>
|
||||||
|
<div class="px-3 py-1 text-sm font-semibold text-dark whitespace-nowrap min-w-140 text-center">
|
||||||
|
{{ dateRangeDisplay }}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-icon mb-0 shadow-none border-0 hover:bg-gray-100 rounded-md"
|
||||||
|
@click="$emit('next-week')"
|
||||||
|
>
|
||||||
|
<i class="fas fa-chevron-right text-secondary text-xs"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, defineProps, defineEmits } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
currentDate: {
|
||||||
|
type: Date,
|
||||||
|
default: () => new Date()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
defineEmits(["prev-week", "next-week"]);
|
||||||
|
|
||||||
|
const dateRangeDisplay = computed(() => {
|
||||||
|
const current = new Date(props.currentDate);
|
||||||
|
const dayOfWeek = current.getDay(); // 0 is Sunday
|
||||||
|
|
||||||
|
// Calculate Monday of current week
|
||||||
|
const diff = current.getDate() - dayOfWeek + (dayOfWeek === 0 ? -6 : 1);
|
||||||
|
const monday = new Date(current);
|
||||||
|
monday.setDate(diff);
|
||||||
|
|
||||||
|
// Calculate Sunday of current week
|
||||||
|
const sunday = new Date(monday);
|
||||||
|
sunday.setDate(monday.getDate() + 6);
|
||||||
|
|
||||||
|
const options = { day: 'numeric', month: 'short' };
|
||||||
|
const startStr = monday.toLocaleDateString('fr-FR', options);
|
||||||
|
|
||||||
|
// If same month, don't repeat month in start date (optional refinement, sticking to simple for now)
|
||||||
|
const endStr = sunday.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short', year: 'numeric' });
|
||||||
|
|
||||||
|
return `${startStr} - ${endStr}`;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.rounded-lg {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rounded-md {
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-xs {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-sm {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-semibold {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.min-w-140 {
|
||||||
|
min-width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover\:bg-gray-100:hover {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,191 @@
|
|||||||
|
<template>
|
||||||
|
<div class="planning-kanban-container mt-3">
|
||||||
|
<div class="py-2 min-vh-100 d-inline-flex" style="overflow-x: auto">
|
||||||
|
<div id="planningKanban"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
/* eslint-disable */
|
||||||
|
import { onMounted, watch, defineProps, defineEmits } from "vue";
|
||||||
|
import "jkanban/dist/jkanban.min.js";
|
||||||
|
import "jkanban/dist/jkanban.min.css";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
interventions: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["edit", "update-status"]);
|
||||||
|
|
||||||
|
let kanbanInstance = null;
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ id: 'todo', title: 'À planifier', status: 'En attente', colorClass: 'warning' },
|
||||||
|
{ id: 'planned', title: 'Confirmé', status: 'Confirmé', colorClass: 'info' },
|
||||||
|
{ id: 'in-progress', title: 'En cours', status: 'En cours', colorClass: 'primary' },
|
||||||
|
{ id: 'done', title: 'Terminé', status: 'Terminé', colorClass: 'success' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const getBoards = () => {
|
||||||
|
return columns.map(col => ({
|
||||||
|
id: col.id,
|
||||||
|
title: col.title,
|
||||||
|
class: col.colorClass, // custom class for header styling if needed
|
||||||
|
item: props.interventions
|
||||||
|
.filter(i => i.status === col.status)
|
||||||
|
.map(i => ({
|
||||||
|
id: i.id.toString(),
|
||||||
|
title: buildCardHtml(i),
|
||||||
|
originalData: i // Store original data for easy access
|
||||||
|
}))
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildCardHtml = (item) => {
|
||||||
|
// Helpers
|
||||||
|
const formatTime = (d) => new Date(d).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' });
|
||||||
|
const formatDate = (d) => new Date(d).toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' });
|
||||||
|
const getInitials = (n) => n ? n.split(' ').map(s => s[0]).join('').substring(0, 2).toUpperCase() : "?";
|
||||||
|
|
||||||
|
const typeColors = {
|
||||||
|
'Soin': '#3b82f6',
|
||||||
|
'Transport': '#10b981',
|
||||||
|
'Mise en bière': '#f59e0b',
|
||||||
|
'Cérémonie': '#8b5cf6'
|
||||||
|
};
|
||||||
|
const typeColor = typeColors[item.type] || '#6b7280';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span class="badge badge-sm bg-gradient-light text-dark border py-1">${item.type}</span>
|
||||||
|
<span class="text-xs font-weight-bold text-secondary">${formatTime(item.date)}</span>
|
||||||
|
</div>
|
||||||
|
<h6 class="mb-1 text-sm font-weight-bold">${item.deceased || 'Non spécifié'}</h6>
|
||||||
|
<p class="text-xs text-secondary mb-3 text-truncate">${item.client || '-'}</p>
|
||||||
|
|
||||||
|
<div class="d-flex align-items-center justify-content-between border-top pt-2">
|
||||||
|
<div class="d-flex align-items-center gap-1">
|
||||||
|
<i class="far fa-calendar text-xs text-secondary"></i>
|
||||||
|
<span class="text-xs text-secondary">${formatDate(item.date)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="avatar avatar-xs rounded-circle bg-gray-200 text-xs d-flex align-items-center justify-content-center" title="${item.collaborator}">
|
||||||
|
${getInitials(item.collaborator)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-left-strip" style="background-color: ${typeColor}; position: absolute; left: 0; top: 0; bottom: 0; width: 4px;"></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const initKanban = () => {
|
||||||
|
const boards = getBoards();
|
||||||
|
|
||||||
|
// Clean up existing if any (jkanban doesn't have a destroy method easily accessible, but we can empty container)
|
||||||
|
const container = document.getElementById("planningKanban");
|
||||||
|
if (container) container.innerHTML = "";
|
||||||
|
|
||||||
|
kanbanInstance = new jKanban({
|
||||||
|
element: "#planningKanban",
|
||||||
|
gutter: "10px",
|
||||||
|
widthBoard: "300px",
|
||||||
|
responsivePercentage: false,
|
||||||
|
dragItems: true,
|
||||||
|
boards: boards,
|
||||||
|
click: (el) => {
|
||||||
|
const id = el.getAttribute("data-eid");
|
||||||
|
const item = props.interventions.find(i => i.id.toString() === id);
|
||||||
|
if (item) {
|
||||||
|
emit("edit", item);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dropEl: (el, target, source, sibling) => {
|
||||||
|
const id = el.getAttribute("data-eid");
|
||||||
|
const targetBoardId = target.parentElement.getAttribute("data-id");
|
||||||
|
|
||||||
|
// Find new status based on target board ID
|
||||||
|
const targetCol = columns.find(c => c.id === targetBoardId);
|
||||||
|
if (targetCol) {
|
||||||
|
emit("update-status", { id, status: targetCol.status });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Apply custom styling adjustments after render
|
||||||
|
applyCustomStyles();
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyCustomStyles = () => {
|
||||||
|
// Helper to add Bootstrap classes or custom styles to jKanban generated elements
|
||||||
|
const headers = document.querySelectorAll('.kanban-board-header');
|
||||||
|
headers.forEach(header => {
|
||||||
|
header.classList.add('bg-transparent', 'border-0', 'pb-2');
|
||||||
|
// Add dot
|
||||||
|
const boardId = header.parentElement.getAttribute('data-id');
|
||||||
|
const col = columns.find(c => c.id === boardId);
|
||||||
|
if(col) {
|
||||||
|
// Logic to inject the dot and count if we want to replicate the exact header design
|
||||||
|
// Note: jKanban header usually just contains title text. We might want to customize title HTML in getBoards().
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// Give DOM a moment
|
||||||
|
setTimeout(() => {
|
||||||
|
initKanban();
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => props.interventions, () => {
|
||||||
|
// Re-init on data change
|
||||||
|
// Optimization: use kanbanInstance methods to add/remove, but full re-init is safer for consistency for now
|
||||||
|
initKanban();
|
||||||
|
}, { deep: true });
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Global styles for jKanban overrides */
|
||||||
|
.kanban-container {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-board {
|
||||||
|
background: transparent !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-board-header {
|
||||||
|
padding-bottom: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-item {
|
||||||
|
background: white !important;
|
||||||
|
border-radius: 0.75rem !important; /* rounded-xl */
|
||||||
|
margin-bottom: 10px !important;
|
||||||
|
padding: 1rem !important;
|
||||||
|
box-shadow: 0 .125rem .25rem rgba(0,0,0,.075) !important;
|
||||||
|
border: 0 !important;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-item:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05) !important;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-drag {
|
||||||
|
min-height: 500px;
|
||||||
|
background-color: #f1f5f9; /* slate-100 */
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
<template>
|
||||||
|
<div class="card border-0 shadow-md rounded-xl">
|
||||||
|
<div class="card-header bg-white border-0 pb-0">
|
||||||
|
<div class="d-flex align-items-center gap-2 mb-2">
|
||||||
|
<h3 class="text-sm font-semibold mb-0">🏖️ Légende congés</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body pt-2">
|
||||||
|
<div class="d-flex flex-wrap gap-3">
|
||||||
|
<div v-for="item in legend" :key="item.label" class="d-flex align-items-center gap-2">
|
||||||
|
<span class="text-sm">{{ item.emoji }}</span>
|
||||||
|
<div class="legend-bar rounded-pill" :style="{ backgroundColor: item.color }"></div>
|
||||||
|
<span class="text-xs text-secondary">{{ item.label }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const legend = [
|
||||||
|
{ emoji: "🏖️", label: "Congé payé", color: "#3b82f6" },
|
||||||
|
{ emoji: "🏥", label: "Maladie", color: "#ef4444" },
|
||||||
|
{ emoji: "📚", label: "Formation", color: "#8b5cf6" },
|
||||||
|
{ emoji: "💼", label: "Sans solde", color: "#6b7280" },
|
||||||
|
{ emoji: "🚑", label: "Accident travail", color: "#dc2626" }
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.card {
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-sm {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-xs {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-semibold {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-bar {
|
||||||
|
width: 2rem;
|
||||||
|
height: 0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rounded-xl {
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gap-3 {
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,141 @@
|
|||||||
|
<template>
|
||||||
|
<div class="planning-list card border-0 shadow-sm rounded-xl">
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table align-items-center mb-0">
|
||||||
|
<thead class="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 ps-4">Date & Heure</th>
|
||||||
|
<th class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 ps-2">Type</th>
|
||||||
|
<th class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 ps-2">Défunt / Client</th>
|
||||||
|
<th class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 ps-2">Collaborateur</th>
|
||||||
|
<th class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 ps-2">Statut</th>
|
||||||
|
<th class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 ps-2">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-if="interventions.length === 0">
|
||||||
|
<td colspan="6" class="text-center py-5">
|
||||||
|
<span class="text-muted text-sm">Aucune intervention pour cette période</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-for="intervention in interventions" :key="intervention.id" class="hover-row transition-all">
|
||||||
|
<td class="ps-4">
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<h6 class="mb-0 text-sm font-weight-bold">{{ formatDate(intervention.date) }}</h6>
|
||||||
|
<span class="text-xs text-secondary">{{ formatTime(intervention.date) }}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<span class="badge badge-sm bg-gradient-light text-dark mb-0 d-flex align-items-center gap-1 border">
|
||||||
|
<span class="width-8-px height-8-px rounded-circle me-1" :style="{ backgroundColor: getTypeColor(intervention.type) }"></span>
|
||||||
|
{{ intervention.type }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<h6 class="mb-0 text-sm">{{ intervention.deceased || 'Non spécifié' }}</h6>
|
||||||
|
<span class="text-xs text-secondary">Client: {{ intervention.client || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="avatar avatar-xs me-2 bg-gradient-primary rounded-circle text-white d-flex align-items-center justify-content-center text-xxs">
|
||||||
|
{{ getInitials(intervention.collaborator) }}
|
||||||
|
</div>
|
||||||
|
<span class="text-sm font-weight-bold text-secondary">{{ intervention.collaborator }}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge badge-sm" :class="getStatusBadgeClass(intervention.status)">
|
||||||
|
{{ intervention.status }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-link text-secondary mb-0 px-2" @click="$emit('edit', intervention)">
|
||||||
|
<i class="fas fa-edit text-xs"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineProps, defineEmits } from "vue";
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
interventions: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
defineEmits(["edit"]);
|
||||||
|
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
if (!dateString) return "-";
|
||||||
|
return new Date(dateString).toLocaleDateString('fr-FR', { weekday: 'long', day: 'numeric', month: 'short' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTime = (dateString) => {
|
||||||
|
if (!dateString) return "-";
|
||||||
|
return new Date(dateString).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const getInitials = (name) => {
|
||||||
|
if (!name) return "?";
|
||||||
|
return name.split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTypeColor = (type) => {
|
||||||
|
const colors = {
|
||||||
|
'Soin': '#3b82f6',
|
||||||
|
'Transport': '#10b981',
|
||||||
|
'Mise en bière': '#f59e0b',
|
||||||
|
'Cérémonie': '#8b5cf6'
|
||||||
|
};
|
||||||
|
return colors[type] || '#6b7280';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusBadgeClass = (status) => {
|
||||||
|
const map = {
|
||||||
|
'Confirmé': 'bg-gradient-info',
|
||||||
|
'Terminé': 'bg-gradient-success',
|
||||||
|
'En attente': 'bg-gradient-warning',
|
||||||
|
'Annulé': 'bg-gradient-danger'
|
||||||
|
};
|
||||||
|
return map[status] || 'bg-gradient-secondary';
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.text-xxs {
|
||||||
|
font-size: 0.65rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.width-8-px {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
.height-8-px {
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-row:hover {
|
||||||
|
background-color: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transition-all {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rounded-xl {
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,65 @@
|
|||||||
|
<template>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button
|
||||||
|
v-for="view in views"
|
||||||
|
:key="view.id"
|
||||||
|
class="btn d-inline-flex align-items-center justify-content-center gap-2 border-0 shadow-sm transition-all"
|
||||||
|
:class="[
|
||||||
|
activeView === view.id ? 'btn-active' : 'btn-inactive'
|
||||||
|
]"
|
||||||
|
@click="$emit('update:activeView', view.id)"
|
||||||
|
>
|
||||||
|
<i :class="view.icon"></i>
|
||||||
|
<span class="d-none d-sm-inline">{{ view.label }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineProps, defineEmits } from "vue";
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
activeView: {
|
||||||
|
type: String,
|
||||||
|
default: "grille"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
defineEmits(["update:activeView"]);
|
||||||
|
|
||||||
|
const views = [
|
||||||
|
{ id: "liste", label: "Liste", icon: "fas fa-list", color: "gray" },
|
||||||
|
{ id: "kanban", label: "Kanban", icon: "fas fa-columns", color: "gray" },
|
||||||
|
{ id: "grille", label: "Grille", icon: "fas fa-calendar-alt", color: "indigo" }
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.btn {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-active {
|
||||||
|
background-color: #4f46e5;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-inactive {
|
||||||
|
background-color: white;
|
||||||
|
color: #64748b;
|
||||||
|
border: 1px solid #e2e8f0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-inactive:hover {
|
||||||
|
background-color: #f8fafc;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transition-all {
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,158 @@
|
|||||||
|
<template>
|
||||||
|
<div class="calendar-grid-container card h-100 border-0 shadow-sm rounded-xl">
|
||||||
|
<div class="card-body p-3 h-100">
|
||||||
|
<div ref="calendarEl" id="fullCalendarGrid" class="h-100"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, watch, defineProps, defineEmits } from "vue";
|
||||||
|
import { Calendar } from "@fullcalendar/core";
|
||||||
|
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||||
|
import interactionPlugin from "@fullcalendar/interaction";
|
||||||
|
import frLocale from "@fullcalendar/core/locales/fr";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
startDate: {
|
||||||
|
type: Date,
|
||||||
|
default: () => new Date()
|
||||||
|
},
|
||||||
|
interventions: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["cell-click", "edit"]);
|
||||||
|
|
||||||
|
const calendarEl = ref(null);
|
||||||
|
let calendar = null;
|
||||||
|
|
||||||
|
const initializeCalendar = () => {
|
||||||
|
if (calendar) {
|
||||||
|
calendar.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!calendarEl.value) return;
|
||||||
|
|
||||||
|
calendar = new Calendar(calendarEl.value, {
|
||||||
|
plugins: [timeGridPlugin, interactionPlugin],
|
||||||
|
initialView: "timeGridWeek",
|
||||||
|
locale: frLocale,
|
||||||
|
headerToolbar: false, // Hidden header, controlled by parent navigator
|
||||||
|
initialDate: props.startDate,
|
||||||
|
allDaySlot: false,
|
||||||
|
slotMinTime: "07:00:00",
|
||||||
|
slotMaxTime: "21:00:00",
|
||||||
|
height: "auto",
|
||||||
|
contentHeight: "auto",
|
||||||
|
expandRows: true,
|
||||||
|
stickyHeaderDates: true,
|
||||||
|
dayHeaderFormat: { weekday: 'long', day: 'numeric', month: 'short' }, // "Lundi 12 janv."
|
||||||
|
events: mapEvents(props.interventions),
|
||||||
|
eventClick: (info) => {
|
||||||
|
const originalEvent = info.event.extendedProps.originalData;
|
||||||
|
emit("edit", originalEvent);
|
||||||
|
},
|
||||||
|
dateClick: (info) => {
|
||||||
|
// timeGrid view dateClick gives date with time
|
||||||
|
emit("cell-click", { date: info.date });
|
||||||
|
},
|
||||||
|
// Styling customization via class names injection if needed
|
||||||
|
eventClassNames: (arg) => {
|
||||||
|
return ['shadow-sm', 'border-0'];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
calendar.render();
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapEvents = (interventions) => {
|
||||||
|
return interventions.map(i => {
|
||||||
|
// Map props.interventions structure to FullCalendar event object
|
||||||
|
// Assuming intervention has: id, date (ISO string), title (or type), status, color
|
||||||
|
const typeColors = {
|
||||||
|
'Soin': '#3b82f6',
|
||||||
|
'Transport': '#10b981',
|
||||||
|
'Mise en bière': '#f59e0b',
|
||||||
|
'Cérémonie': '#8b5cf6'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Default duration 1 hour if not specified
|
||||||
|
const start = new Date(i.date);
|
||||||
|
const end = new Date(start.getTime() + 60 * 60 * 1000);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: i.id,
|
||||||
|
title: i.deceased ? `${i.type} - ${i.deceased}` : i.type,
|
||||||
|
start: i.date,
|
||||||
|
end: i.end || end, // Use provided end or default
|
||||||
|
backgroundColor: typeColors[i.type] || '#6b7280',
|
||||||
|
borderColor: typeColors[i.type] || '#6b7280',
|
||||||
|
textColor: '#ffffff',
|
||||||
|
extendedProps: {
|
||||||
|
originalData: i
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initializeCalendar();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => props.startDate, (newDate) => {
|
||||||
|
if (calendar) {
|
||||||
|
calendar.gotoDate(newDate);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => props.interventions, (newInterventions) => {
|
||||||
|
if (calendar) {
|
||||||
|
calendar.removeAllEvents();
|
||||||
|
calendar.addEventSource(mapEvents(newInterventions));
|
||||||
|
}
|
||||||
|
}, { deep: true });
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.calendar-grid-container {
|
||||||
|
min-height: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FullCalendar Custom Overrides to match Soft UI */
|
||||||
|
:deep(.fc-col-header-cell) {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #67748e;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.fc-timegrid-slot) {
|
||||||
|
height: 3rem; /* Taller slots */
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.fc-timegrid-slot-label) {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #8392ab;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.fc-scrollgrid) {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.fc td), :deep(.fc th) {
|
||||||
|
border-color: #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Today highlight */
|
||||||
|
:deep(.fc-day-today) {
|
||||||
|
background-color: rgba(233, 236, 239, 0.3) !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,516 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="table-container">
|
|
||||||
<!-- Loading State -->
|
|
||||||
<div v-if="loading" class="loading-container">
|
|
||||||
<div class="loading-spinner">
|
|
||||||
<div class="spinner-border text-primary" role="status">
|
|
||||||
<span class="visually-hidden">Chargement...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="loading-content">
|
|
||||||
<!-- Skeleton Rows -->
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-flush">
|
|
||||||
<thead class="thead-light">
|
|
||||||
<tr>
|
|
||||||
<th>Code</th>
|
|
||||||
<th>Nom</th>
|
|
||||||
<th>Catégorie parent</th>
|
|
||||||
<th>Description</th>
|
|
||||||
<th>Statut</th>
|
|
||||||
<th>Produits</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="i in skeletonRows" :key="i" class="skeleton-row">
|
|
||||||
<!-- Code Column Skeleton -->
|
|
||||||
<td>
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<div class="skeleton-avatar"></div>
|
|
||||||
<div class="skeleton-text medium ms-2"></div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<!-- Name Column Skeleton -->
|
|
||||||
<td>
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<div class="skeleton-avatar"></div>
|
|
||||||
<div class="skeleton-text long ms-2"></div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<!-- Parent Category Column Skeleton -->
|
|
||||||
<td>
|
|
||||||
<div class="skeleton-text medium"></div>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<!-- Description Column Skeleton -->
|
|
||||||
<td>
|
|
||||||
<div class="skeleton-text very-long"></div>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<!-- Status Column Skeleton -->
|
|
||||||
<td>
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<div class="skeleton-icon"></div>
|
|
||||||
<div class="skeleton-text short ms-2"></div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<!-- Products Count Column Skeleton -->
|
|
||||||
<td>
|
|
||||||
<div class="skeleton-text short"></div>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<!-- Actions Column Skeleton -->
|
|
||||||
<td>
|
|
||||||
<div class="d-flex gap-1">
|
|
||||||
<div class="skeleton-icon small"></div>
|
|
||||||
<div class="skeleton-icon small"></div>
|
|
||||||
<div class="skeleton-icon small"></div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Data State -->
|
|
||||||
<div v-else class="table-responsive">
|
|
||||||
<table id="product-category-list" class="table table-flush">
|
|
||||||
<thead class="thead-light">
|
|
||||||
<tr>
|
|
||||||
<th>Code</th>
|
|
||||||
<th>Nom</th>
|
|
||||||
<th>Catégorie parent</th>
|
|
||||||
<th>Description</th>
|
|
||||||
<th>Statut</th>
|
|
||||||
<th>Produits</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="category in data" :key="category.id">
|
|
||||||
<!-- Code Column -->
|
|
||||||
<td class="font-weight-bold">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<span>{{ category.code }}</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<!-- Name Column -->
|
|
||||||
<td class="font-weight-bold">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<div>
|
|
||||||
<div class="text-sm font-weight-bold">
|
|
||||||
{{ category.name }}
|
|
||||||
</div>
|
|
||||||
<div class="text-xs text-muted">ID: {{ category.id }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<!-- Parent Category Column -->
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<div v-if="category.parent" class="d-flex align-items-center">
|
|
||||||
<soft-button
|
|
||||||
color="info"
|
|
||||||
variant="outline"
|
|
||||||
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
|
|
||||||
>
|
|
||||||
<i class="fas fa-folder" aria-hidden="true"></i>
|
|
||||||
</soft-button>
|
|
||||||
<span>{{ category.parent.name }}</span>
|
|
||||||
</div>
|
|
||||||
<div v-else class="text-muted">
|
|
||||||
<soft-button
|
|
||||||
color="secondary"
|
|
||||||
variant="outline"
|
|
||||||
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
|
|
||||||
>
|
|
||||||
<i class="fas fa-folder-open" aria-hidden="true"></i>
|
|
||||||
</soft-button>
|
|
||||||
Catégorie racine
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<!-- Description Column -->
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<div class="description-cell">
|
|
||||||
<span
|
|
||||||
v-if="category.description"
|
|
||||||
class="text-truncate d-inline-block"
|
|
||||||
style="max-width: 200px"
|
|
||||||
>
|
|
||||||
{{ category.description }}
|
|
||||||
</span>
|
|
||||||
<span v-else class="text-muted">Aucune description</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<!-- Status Column -->
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<div class="d-flex flex-column">
|
|
||||||
<!-- Status Badge -->
|
|
||||||
<span :class="getStatusClass(category)">
|
|
||||||
<i
|
|
||||||
:class="category.active ? 'fas fa-check me-1' : 'fas fa-times me-1'"
|
|
||||||
></i>
|
|
||||||
{{ category.active ? "Active" : "Inactive" }}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<!-- Children Badge -->
|
|
||||||
<span
|
|
||||||
v-if="category.has_children"
|
|
||||||
class="badge badge-info mt-1"
|
|
||||||
>
|
|
||||||
<i class="fas fa-sitemap me-1"></i>
|
|
||||||
{{ category.children_count || 0 }} enfants
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<!-- Products Count Column -->
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<soft-badge
|
|
||||||
:color="category.has_products ? 'success' : 'secondary'"
|
|
||||||
class="me-2"
|
|
||||||
>
|
|
||||||
{{ category.products_count || 0 }}
|
|
||||||
</soft-badge>
|
|
||||||
<span class="text-muted">produits</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<div class="d-flex align-items-center gap-2">
|
|
||||||
<!-- View Button -->
|
|
||||||
<soft-button
|
|
||||||
color="info"
|
|
||||||
variant="outline"
|
|
||||||
title="Voir la catégorie"
|
|
||||||
:data-category-id="category.id"
|
|
||||||
class="btn-icon-only btn-rounded mb-0 btn-sm d-flex align-items-center justify-content-center"
|
|
||||||
@click="emit('view', category)"
|
|
||||||
>
|
|
||||||
<i class="fas fa-eye" aria-hidden="true"></i>
|
|
||||||
</soft-button>
|
|
||||||
|
|
||||||
<!-- Edit Button -->
|
|
||||||
<soft-button
|
|
||||||
color="warning"
|
|
||||||
variant="outline"
|
|
||||||
title="Modifier la catégorie"
|
|
||||||
:data-category-id="category.id"
|
|
||||||
class="btn-icon-only btn-rounded mb-0 btn-sm d-flex align-items-center justify-content-center"
|
|
||||||
@click="emit('edit', category)"
|
|
||||||
>
|
|
||||||
<i class="fas fa-edit" aria-hidden="true"></i>
|
|
||||||
</soft-button>
|
|
||||||
|
|
||||||
<!-- Delete Button -->
|
|
||||||
<soft-button
|
|
||||||
color="danger"
|
|
||||||
variant="outline"
|
|
||||||
title="Supprimer la catégorie"
|
|
||||||
:data-category-id="category.id"
|
|
||||||
class="btn-icon-only btn-rounded mb-0 btn-sm d-flex align-items-center justify-content-center"
|
|
||||||
@click="emit('delete', category)"
|
|
||||||
>
|
|
||||||
<i class="fas fa-trash" aria-hidden="true"></i>
|
|
||||||
</soft-button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Empty State -->
|
|
||||||
<div v-if="!loading && data.length === 0" class="empty-state">
|
|
||||||
<div class="empty-icon">
|
|
||||||
<i class="fas fa-tags fa-3x text-muted"></i>
|
|
||||||
</div>
|
|
||||||
<h5 class="empty-title">Aucune catégorie de produit trouvée</h5>
|
|
||||||
<p class="empty-text text-muted">
|
|
||||||
Aucune catégorie de produit à afficher pour le moment.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, onMounted, watch, onUnmounted } from "vue";
|
|
||||||
import { DataTable } from "simple-datatables";
|
|
||||||
import SoftButton from "@/components/SoftButton.vue";
|
|
||||||
import SoftAvatar from "@/components/SoftAvatar.vue";
|
|
||||||
import SoftBadge from "@/components/SoftBadge.vue";
|
|
||||||
import { defineProps, defineEmits } from "vue";
|
|
||||||
|
|
||||||
const emit = defineEmits(["view", "edit", "delete"]);
|
|
||||||
|
|
||||||
// Sample avatar images for categories
|
|
||||||
import img1 from "@/assets/img/team-2.jpg";
|
|
||||||
import img2 from "@/assets/img/team-1.jpg";
|
|
||||||
import img3 from "@/assets/img/team-3.jpg";
|
|
||||||
import img4 from "@/assets/img/team-4.jpg";
|
|
||||||
import img5 from "@/assets/img/team-5.jpg";
|
|
||||||
import img6 from "@/assets/img/ivana-squares.jpg";
|
|
||||||
|
|
||||||
const avatarImages = [img1, img2, img3, img4, img5, img6];
|
|
||||||
|
|
||||||
// Reactive data
|
|
||||||
const dataTableInstance = ref(null);
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
data: {
|
|
||||||
type: Array,
|
|
||||||
default: [],
|
|
||||||
},
|
|
||||||
loading: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
skeletonRows: {
|
|
||||||
type: Number,
|
|
||||||
default: 5,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Methods
|
|
||||||
const getRandomAvatar = () => {
|
|
||||||
const randomIndex = Math.floor(Math.random() * avatarImages.length);
|
|
||||||
return avatarImages[randomIndex];
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusClass = (category) => {
|
|
||||||
return category.active ? "badge badge-success" : "badge badge-danger";
|
|
||||||
};
|
|
||||||
|
|
||||||
const initializeDataTable = () => {
|
|
||||||
// Destroy existing instance if it exists
|
|
||||||
if (dataTableInstance.value) {
|
|
||||||
dataTableInstance.value.destroy();
|
|
||||||
dataTableInstance.value = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dataTableEl = document.getElementById("product-category-list");
|
|
||||||
if (dataTableEl) {
|
|
||||||
dataTableInstance.value = new DataTable(dataTableEl, {
|
|
||||||
searchable: true,
|
|
||||||
fixedHeight: true,
|
|
||||||
perPage: 10,
|
|
||||||
perPageSelect: [5, 10, 15, 20],
|
|
||||||
});
|
|
||||||
|
|
||||||
dataTableEl.addEventListener("click", handleTableClick);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTableClick = (event) => {
|
|
||||||
const button = event.target.closest("button");
|
|
||||||
if (!button) return;
|
|
||||||
const categoryId = button.getAttribute("data-category-id");
|
|
||||||
|
|
||||||
if (
|
|
||||||
button.title === "Supprimer la catégorie" ||
|
|
||||||
button.querySelector(".fa-trash")
|
|
||||||
) {
|
|
||||||
emit("delete", categoryId);
|
|
||||||
} else if (
|
|
||||||
button.title === "Modifier la catégorie" ||
|
|
||||||
button.querySelector(".fa-edit")
|
|
||||||
) {
|
|
||||||
emit("edit", categoryId);
|
|
||||||
} else if (
|
|
||||||
button.title === "Voir la catégorie" ||
|
|
||||||
button.querySelector(".fa-eye")
|
|
||||||
) {
|
|
||||||
emit("view", categoryId);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Watch for data changes to reinitialize datatable
|
|
||||||
watch(
|
|
||||||
() => props.data,
|
|
||||||
() => {
|
|
||||||
if (!props.loading) {
|
|
||||||
// Small delay to ensure DOM is updated
|
|
||||||
setTimeout(() => {
|
|
||||||
initializeDataTable();
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ deep: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
const dataTableEl = document.getElementById("product-category-list");
|
|
||||||
if (dataTableEl) {
|
|
||||||
dataTableEl.removeEventListener("click", handleTableClick);
|
|
||||||
}
|
|
||||||
if (dataTableInstance.value) {
|
|
||||||
dataTableInstance.value.destroy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize data
|
|
||||||
onMounted(() => {
|
|
||||||
if (!props.loading && props.data.length > 0) {
|
|
||||||
initializeDataTable();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.table-container {
|
|
||||||
position: relative;
|
|
||||||
min-height: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-container {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-spinner {
|
|
||||||
position: absolute;
|
|
||||||
top: 20px;
|
|
||||||
right: 20px;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-content {
|
|
||||||
opacity: 0.7;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeleton-row {
|
|
||||||
animation: pulse 1.5s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeleton-avatar {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
|
||||||
background-size: 200% 100%;
|
|
||||||
animation: shimmer 2s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeleton-icon {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
|
||||||
background-size: 200% 100%;
|
|
||||||
animation: shimmer 2s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeleton-text {
|
|
||||||
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
|
||||||
background-size: 200% 100%;
|
|
||||||
animation: shimmer 2s infinite;
|
|
||||||
border-radius: 4px;
|
|
||||||
height: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeleton-text.short {
|
|
||||||
width: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeleton-text.medium {
|
|
||||||
width: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeleton-text.long {
|
|
||||||
width: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeleton-text.very-long {
|
|
||||||
width: 160px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state {
|
|
||||||
text-align: center;
|
|
||||||
padding: 3rem 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-icon {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-title {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
color: #6c757d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-text {
|
|
||||||
max-width: 300px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.description-cell {
|
|
||||||
max-width: 200px;
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-xs {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Animations */
|
|
||||||
@keyframes pulse {
|
|
||||||
0% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes shimmer {
|
|
||||||
0% {
|
|
||||||
background-position: -200% 0;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
background-position: 200% 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive adjustments */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.loading-spinner {
|
|
||||||
top: 10px;
|
|
||||||
right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeleton-text.very-long {
|
|
||||||
width: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeleton-text.long {
|
|
||||||
width: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeleton-text.medium {
|
|
||||||
width: 60px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeleton-icon.small {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
|
||||||
background-size: 200% 100%;
|
|
||||||
animation: shimmer 2s infinite;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -0,0 +1,73 @@
|
|||||||
|
<template>
|
||||||
|
<div class="planning-container p-2 p-md-4">
|
||||||
|
<div class="container-max mx-auto">
|
||||||
|
<!-- Header Section -->
|
||||||
|
<div class="d-flex flex-column flex-md-row justify-content-between align-items-start align-items-md-center mb-4 gap-3">
|
||||||
|
<slot name="header"></slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- View Toggles -->
|
||||||
|
<div class="d-flex gap-2 mb-4 overflow-auto pb-2">
|
||||||
|
<slot name="view-toggles"></slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-wrapper">
|
||||||
|
<div class="row g-4">
|
||||||
|
<!-- Sidebar & Legend Column (Left on desktop) -->
|
||||||
|
<!-- <div class="col-12 col-xl-3 order-2 order-xl-1">
|
||||||
|
<div class="sticky-top" style="top: 2rem; z-index: 10;">
|
||||||
|
<div class="mb-4">
|
||||||
|
<slot name="legend"></slot>
|
||||||
|
</div>
|
||||||
|
<slot name="sidebar"></slot>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<!-- Calendar Grid Column (Right on desktop) -->
|
||||||
|
<div class="col-12 col-xl-12 order-1 order-xl-2">
|
||||||
|
<slot name="calendar-grid"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
// Template layout for Planning page
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.planning-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #f0f7ff 0%, #eef2ff 50%, #f5f3ff 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-max {
|
||||||
|
max-width: 1400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gap-3 {
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar for view toggles on mobile */
|
||||||
|
.overflow-auto::-webkit-scrollbar {
|
||||||
|
height: 4px;
|
||||||
|
}
|
||||||
|
.overflow-auto::-webkit-scrollbar-thumb {
|
||||||
|
background: #e2e8f0;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.sticky-top {
|
||||||
|
max-height: calc(100vh - 4rem);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -120,6 +120,14 @@ export default {
|
|||||||
miniIcon: "A",
|
miniIcon: "A",
|
||||||
route: { name: "Agenda" },
|
route: { name: "Agenda" },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "planning",
|
||||||
|
type: "single",
|
||||||
|
text: "Planning",
|
||||||
|
icon: "Office",
|
||||||
|
miniIcon: "P",
|
||||||
|
route: { name: "Planning" },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "courriel",
|
id: "courriel",
|
||||||
type: "single",
|
type: "single",
|
||||||
|
|||||||
@ -401,6 +401,12 @@ const routes = [
|
|||||||
name: "Agenda",
|
name: "Agenda",
|
||||||
component: () => import("@/views/pages/Agenda.vue"),
|
component: () => import("@/views/pages/Agenda.vue"),
|
||||||
},
|
},
|
||||||
|
// Planning
|
||||||
|
{
|
||||||
|
path: "/planning",
|
||||||
|
name: "Planning",
|
||||||
|
component: () => import("@/views/pages/Planning.vue"),
|
||||||
|
},
|
||||||
// Courriel
|
// Courriel
|
||||||
{
|
{
|
||||||
path: "/courriel",
|
path: "/courriel",
|
||||||
|
|||||||
150
thanasoft-front/src/views/pages/Planning.vue
Normal file
150
thanasoft-front/src/views/pages/Planning.vue
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<planning-presentation
|
||||||
|
:intervention-count="interventions.length"
|
||||||
|
:collaborators="collaborators"
|
||||||
|
:interventions="interventions"
|
||||||
|
:current-date="currentDate"
|
||||||
|
v-model:activeView="activeView"
|
||||||
|
@refresh="handleRefresh"
|
||||||
|
@new-request="handleNewRequest"
|
||||||
|
@cell-click="handleCellClick"
|
||||||
|
@prev-week="handlePrevWeek"
|
||||||
|
@next-week="handleNextWeek"
|
||||||
|
@edit-intervention="handleEditIntervention"
|
||||||
|
@update-status="handleUpdateStatus"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from "vue";
|
||||||
|
import PlanningPresentation from "@/components/Organism/Planning/PlanningPresentation.vue";
|
||||||
|
|
||||||
|
// State
|
||||||
|
const interventions = ref([]);
|
||||||
|
const collaborators = ref([]);
|
||||||
|
const currentDate = ref(new Date());
|
||||||
|
const activeView = ref("grille");
|
||||||
|
|
||||||
|
// Lifecycle
|
||||||
|
onMounted(async () => {
|
||||||
|
await fetchData();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const fetchData = async () => {
|
||||||
|
// Mock data for demonstration
|
||||||
|
collaborators.value = [
|
||||||
|
{ id: 1, name: "Jean Dupont" },
|
||||||
|
{ id: 2, name: "Marie Curie" },
|
||||||
|
{ id: 3, name: "Lucas Martin" }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Generate some random interventions around current date
|
||||||
|
const baseDate = new Date(currentDate.value);
|
||||||
|
const startOfWeek = getMonday(baseDate);
|
||||||
|
|
||||||
|
interventions.value = [
|
||||||
|
{
|
||||||
|
id: 101,
|
||||||
|
date: addDays(startOfWeek, 0, 9, 30), // Monday 9:30
|
||||||
|
type: "Soin",
|
||||||
|
deceased: "M. Martin",
|
||||||
|
client: "Pompes Funèbres Générales",
|
||||||
|
collaborator: "Jean Dupont",
|
||||||
|
status: "Confirmé"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 102,
|
||||||
|
date: addDays(startOfWeek, 1, 14, 0), // Tuesday 14:00
|
||||||
|
type: "Transport",
|
||||||
|
deceased: "Mme. Dubois",
|
||||||
|
client: "Roc Eclerc",
|
||||||
|
collaborator: "Marie Curie",
|
||||||
|
status: "En cours"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 103,
|
||||||
|
date: addDays(startOfWeek, 2, 10, 0), // Wednesday 10:00
|
||||||
|
type: "Mise en bière",
|
||||||
|
deceased: "M. Lefebvre",
|
||||||
|
client: "PF Locales",
|
||||||
|
collaborator: "Lucas Martin",
|
||||||
|
status: "En attente"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 104,
|
||||||
|
date: addDays(startOfWeek, 3, 16, 30), // Thursday 16:30
|
||||||
|
type: "Cérémonie",
|
||||||
|
deceased: "Mme. Petit",
|
||||||
|
client: "Famille Petit",
|
||||||
|
collaborator: "Jean Dupont",
|
||||||
|
status: "Terminé"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 105,
|
||||||
|
date: addDays(startOfWeek, 4, 11, 0), // Friday 11:00
|
||||||
|
type: "Soin",
|
||||||
|
deceased: "Inconnu",
|
||||||
|
client: "Police",
|
||||||
|
collaborator: "Marie Curie",
|
||||||
|
status: "Annulé"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRefresh = async () => {
|
||||||
|
await fetchData();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNewRequest = () => {
|
||||||
|
console.log("New request");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCellClick = (info) => {
|
||||||
|
console.log("Cell clicked", info);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditIntervention = (intervention) => {
|
||||||
|
console.log("Edit intervention", intervention);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePrevWeek = () => {
|
||||||
|
const newDate = new Date(currentDate.value);
|
||||||
|
newDate.setDate(newDate.getDate() - 7);
|
||||||
|
currentDate.value = newDate;
|
||||||
|
fetchData(); // Refresh data for new week
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateStatus = (payload) => {
|
||||||
|
const intervention = interventions.value.find(i => i.id.toString() === payload.id);
|
||||||
|
if (intervention) {
|
||||||
|
intervention.status = payload.status;
|
||||||
|
// In a real app, we would make an API call here to persist the change
|
||||||
|
console.log(`Updated status of intervention ${payload.id} to ${payload.status}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNextWeek = () => {
|
||||||
|
const newDate = new Date(currentDate.value);
|
||||||
|
newDate.setDate(newDate.getDate() + 7);
|
||||||
|
currentDate.value = newDate;
|
||||||
|
fetchData(); // Refresh data for new week
|
||||||
|
};
|
||||||
|
|
||||||
|
// Utilities
|
||||||
|
function getMonday(d) {
|
||||||
|
d = new Date(d);
|
||||||
|
var day = d.getDay(),
|
||||||
|
diff = d.getDate() - day + (day == 0 ? -6 : 1); // adjust when day is sunday
|
||||||
|
return new Date(d.setDate(diff));
|
||||||
|
}
|
||||||
|
|
||||||
|
function addDays(date, days, hours, minutes) {
|
||||||
|
const result = new Date(date);
|
||||||
|
result.setDate(result.getDate() + days);
|
||||||
|
result.setHours(hours, minutes, 0, 0);
|
||||||
|
return result.toISOString();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
Loading…
x
Reference in New Issue
Block a user