add gestion thanato

This commit is contained in:
Nyavokevin 2025-11-06 15:09:40 +03:00
parent e55cc5253e
commit 8d1d65e27b
28 changed files with 5941 additions and 312 deletions

View File

@ -0,0 +1,164 @@
<template>
<employee-detail-template>
<template #button-return>
<div class="col-12">
<router-link
to="/employes"
class="btn btn-outline-secondary btn-sm mb-3"
>
<i class="fas fa-arrow-left me-2"></i>Retour aux employés
</router-link>
</div>
</template>
<template #loading-state>
<div v-if="isLoading" class="text-center p-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Chargement...</span>
</div>
</div>
</template>
<template #employee-detail-sidebar>
<EmployeeDetailSidebar
:avatar-url="employeeAvatar"
:initials="getInitials(employee.full_name)"
:employee-name="employee.full_name"
:job-title="employee.job_title"
:status="getEmployeeType(employee)"
:hire-date="formatDate(employee.hire_date)"
:active-tab="activeTab"
:is-active="employee.active"
:is-thanatopractitioner="!!thanatopractitionerData"
@edit-avatar="triggerFileInput"
@change-tab="activeTab = $event"
/>
</template>
<template #file-input>
<input
:ref="fileInput"
type="file"
class="d-none"
accept="image/*"
@change="handleAvatarUpload"
/>
</template>
<template #employee-detail-content>
<EmployeeDetailContent
:active-tab="activeTab"
:employee="employee"
:thanatopractitioner-data="thanatopractitionerData"
:practitioner-documents="practitionerDocuments"
:formatted-hire-date="formatDate(employee.hire_date)"
:employee-id="employee.id"
@change-tab="activeTab = $event"
@updating-employee="handleUpdateEmployee"
@create-practitioner-document="handleCreatePractitionerDocument"
@updating-practitioner-document="handleModifiedPractitionerDocument"
@remove-practitioner-document="handleRemovePractitionerDocument"
/>
</template>
</employee-detail-template>
</template>
<script setup>
import { defineProps, defineEmits, ref } from "vue";
import EmployeeDetailTemplate from "@/components/templates/CRM/EmployeeDetailTemplate.vue";
import EmployeeDetailSidebar from "./employee/EmployeeDetailSidebar.vue";
import EmployeeDetailContent from "./employee/EmployeeDetailContent.vue";
import { RouterLink } from "vue-router";
const props = defineProps({
employee: {
type: Object,
required: true,
},
isLoading: {
type: Boolean,
default: false,
},
employeeAvatar: {
type: String,
default: "",
},
activeTab: {
type: String,
default: "overview",
},
fileInput: {
type: Object,
required: true,
},
thanatopractitionerData: {
type: Object,
default: null,
},
practitionerDocuments: {
type: Array,
default: () => [],
},
});
const localAvatar = ref(props.employeeAvatar);
const emit = defineEmits([
"updateTheEmployee",
"create-practitioner-document",
"updating-practitioner-document",
"remove-practitioner-document",
]);
const handleAvatarUpload = (event) => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
localAvatar.value = e.target.result;
// TODO: Upload to server
console.log("Upload avatar to server");
};
reader.readAsDataURL(file);
}
};
const handleUpdateEmployee = (updateData) => {
emit("updateTheEmployee", updateData);
};
const handleCreatePractitionerDocument = (data) => {
emit("create-practitioner-document", data);
};
const handleModifiedPractitionerDocument = (modifiedDocument) => {
emit("updating-practitioner-document", modifiedDocument);
};
const handleRemovePractitionerDocument = (documentId) => {
emit("remove-practitioner-document", documentId);
};
const getInitials = (name) => {
if (!name) return "?";
return name
.split(" ")
.map((word) => word[0])
.join("")
.toUpperCase()
.substring(0, 2);
};
const getEmployeeType = (employee) => {
if (employee.thanatopractitioner) {
return "Thanatopractitioner";
}
return "Employé";
};
const formatDate = (dateString) => {
if (!dateString) return "Non renseignée";
const date = new Date(dateString);
return date.toLocaleDateString("fr-FR", {
year: "numeric",
month: "long",
day: "numeric",
});
};
</script>

View File

@ -0,0 +1,105 @@
<template>
<div>
<!-- Overview Tab -->
<div v-show="activeTab === 'overview'">
<EmployeeOverview
:employee="employee"
:formatted-hire-date="formattedHireDate"
:employee-id="employeeId"
@view-info-tab="$emit('change-tab', 'info')"
/>
</div>
<!-- Information Tab -->
<div v-show="activeTab === 'info'">
<EmployeeInfoTab
:employee="employee"
@employee-updated="updateEmployee"
/>
</div>
<!-- Documents Tab -->
<div v-show="activeTab === 'documents'">
<EmployeeDocumentsTab
:documents="practitionerDocuments"
:employee-id="employee.id"
@document-created="handleCreateDocument"
@document-modified="handleModifiedDocument"
@document-removed="handleRemoveDocument"
/>
</div>
<!-- Practitioner Tab (Only for thanatopractitioners) -->
<div v-show="activeTab === 'practitioner' && thanatopractitionerData">
<!-- <EmployeePractitionerTab
:thanatopractitioner="thanatopractitionerData"
:employee="employee"
/> -->
</div>
<!-- Activity Tab -->
<div v-show="activeTab === 'activity'">
<EmployeeActivityTab :employee="employee" />
</div>
</div>
</template>
<script setup>
import EmployeeOverview from "@/components/molecules/employee/EmployeeOverview.vue";
import EmployeeInfoTab from "@/components/molecules/employee/EmployeeInfoTab.vue";
import EmployeeDocumentsTab from "@/components/molecules/employee/EmployeeDocumentsTab.vue";
import EmployeePractitionerTab from "@/components/molecules/employee/EmployeePractitionerTab.vue";
import { defineProps, defineEmits } from "vue";
import EmployeeActivityTab from "@/components/molecules/employee/EmployeeActivityTab.vue";
defineProps({
activeTab: {
type: String,
required: true,
},
employee: {
type: Object,
required: true,
},
thanatopractitionerData: {
type: Object,
default: null,
},
practitionerDocuments: {
type: Array,
default: () => [],
},
formattedHireDate: {
type: String,
default: "",
},
employeeId: {
type: [Number, String],
required: true,
},
});
const emit = defineEmits([
"change-tab",
"updating-employee",
"create-practitioner-document",
"updating-practitioner-document",
"remove-practitioner-document",
]);
const updateEmployee = (updatedEmployee) => {
emit("updating-employee", updatedEmployee);
};
const handleCreateDocument = (newDocument) => {
emit("create-practitioner-document", newDocument);
};
const handleModifiedDocument = (modifiedDocument) => {
emit("updating-practitioner-document", modifiedDocument);
};
const handleRemoveDocument = (documentId) => {
emit("remove-practitioner-document", documentId);
};
</script>

View File

@ -0,0 +1,85 @@
<template>
<div class="card position-sticky top-1">
<!-- Employee Profile Card -->
<EmployeeProfileCard
:avatar-url="avatarUrl"
:initials="initials"
:employee-name="employeeName"
:job-title="jobTitle"
:status="status"
:hire-date="hireDate"
:is-active="isActive"
:is-thanatopractitioner="isThanatopractitioner"
@edit-avatar="$emit('edit-avatar')"
/>
<hr class="horizontal dark my-3 mx-3" />
<!-- Tab Navigation -->
<div class="card-body pt-0">
<EmployeeTabNavigation
:active-tab="activeTab"
:is-thanatopractitioner="isThanatopractitioner"
@change-tab="$emit('change-tab', $event)"
/>
</div>
</div>
</template>
<script setup>
import EmployeeProfileCard from "@/components/molecules/employee/EmployeeProfileCard.vue";
import EmployeeTabNavigation from "@/components/molecules/employee/EmployeeTabNavigation.vue";
import { defineProps, defineEmits } from "vue";
defineProps({
avatarUrl: {
type: String,
default: null,
},
initials: {
type: String,
required: true,
},
employeeName: {
type: String,
required: true,
},
jobTitle: {
type: String,
default: "Employé",
},
status: {
type: String,
default: "Actif",
},
hireDate: {
type: String,
required: true,
},
isActive: {
type: Boolean,
default: true,
},
isThanatopractitioner: {
type: Boolean,
default: false,
},
activeTab: {
type: String,
required: true,
},
});
defineEmits(["edit-avatar", "change-tab"]);
</script>
<style scoped>
.position-sticky {
top: 1rem;
}
.card {
border: 0;
box-shadow: 0 0 2rem 0 rgba(136, 152, 170, 0.15);
}
</style>

View File

@ -13,62 +13,11 @@
<employee-table
:data="employeeData"
:loading="loadingData"
:pagination="pagination"
@view="goToDetails"
@delete="deleteEmployee"
@changePage="handleChangePage"
/>
<!-- Pagination Component -->
<div v-if="pagination && pagination.total > 0" class="mt-4 px-4">
<nav aria-label="Employee pagination">
<ul class="pagination justify-content-center mb-0">
<li
class="page-item"
:class="{ disabled: pagination.current_page === 1 }"
>
<button
class="page-link"
@click="changePage(pagination.current_page - 1)"
:disabled="pagination.current_page === 1"
>
<i class="fas fa-chevron-left"></i>
</button>
</li>
<li
v-for="page in visiblePages"
:key="page"
class="page-item"
:class="{ active: page === pagination.current_page }"
>
<button class="page-link" @click="changePage(page)">
{{ page }}
</button>
</li>
<li
class="page-item"
:class="{
disabled: pagination.current_page === pagination.last_page,
}"
>
<button
class="page-link"
@click="changePage(pagination.current_page + 1)"
:disabled="pagination.current_page === pagination.last_page"
>
<i class="fas fa-chevron-right"></i>
</button>
</li>
</ul>
</nav>
<!-- Pagination Info -->
<div class="text-center mt-2">
<small class="text-muted">
Affichage de {{ pagination.from }} à {{ pagination.to }} sur
{{ pagination.total }} employés
</small>
</div>
</div>
</template>
</employee-template>
</template>
@ -78,13 +27,12 @@ import EmployeeTable from "@/components/molecules/Employees/EmployeeTable.vue";
import addButton from "@/components/molecules/new-button/addButton.vue";
import FilterTable from "@/components/molecules/Tables/FilterTable.vue";
import TableAction from "@/components/molecules/Tables/TableAction.vue";
import { computed } from "vue";
import { defineProps, defineEmits } from "vue";
import { useRouter } from "vue-router";
const router = useRouter();
const emit = defineEmits(["pushDetails", "deleteEmployee"]);
const emit = defineEmits(["pushDetails", "deleteEmployee", "changePage"]);
const props = defineProps({
employeeData: {
@ -112,117 +60,25 @@ const goToDetails = (employeeId) => {
};
const deleteEmployee = (employeeId) => {
console.log(
"deleteEmployee called in EmployeePresentation with ID:",
employeeId
);
emit("deleteEmployee", employeeId);
};
// Computed property for visible page numbers
const visiblePages = computed(() => {
if (!props.pagination) return [];
const current = props.pagination.current_page;
const last = props.pagination.last_page;
const delta = 2; // Number of pages to show on each side of current page
const range = [];
const rangeWithDots = [];
// Calculate range around current page
for (
let i = Math.max(2, current - delta);
i <= Math.min(last - 1, current + delta);
i++
) {
range.push(i);
}
// Add first page
if (current - delta > 2) {
rangeWithDots.push(1, "...");
} else {
rangeWithDots.push(1);
}
// Add calculated range
rangeWithDots.push(...range);
// Add last page
if (current + delta < last - 1) {
rangeWithDots.push("...", last);
} else if (last > 1) {
rangeWithDots.push(last);
}
return rangeWithDots;
});
const changePage = (page) => {
if (page >= 1 && page <= props.pagination.last_page) {
// Emit event for parent component to handle page change
const handleChangePage = (page) => {
console.log(
"handleChangePage called in EmployeePresentation with page:",
page
);
if (page >= 1 && page <= (props.pagination?.last_page || 1)) {
console.log("Emitting changePage event from EmployeePresentation:", page);
emit("changePage", page);
}
};
</script>
<style scoped>
.pagination {
margin-bottom: 0;
}
.page-link {
border: 1px solid #dee2e6;
color: #5e72e4;
text-decoration: none;
background-color: #fff;
transition: all 0.15s ease-in-out;
}
.page-link:hover {
color: #5e72e4;
background-color: #e9ecef;
border-color: #dee2e6;
}
.page-item.active .page-link {
background-color: #5e72e4;
border-color: #5e72e4;
color: white;
}
.page-item.disabled .page-link {
color: #6c757d;
background-color: #fff;
border-color: #dee2e6;
pointer-events: none;
}
.page-item:first-child .page-link {
border-top-left-radius: 0.25rem;
border-bottom-left-radius: 0.25rem;
}
.page-item:last-child .page-link {
border-top-right-radius: 0.25rem;
border-bottom-right-radius: 0.25rem;
}
.mt-4 {
margin-top: 1.5rem;
}
.px-4 {
padding-left: 1.5rem;
padding-right: 1.5rem;
}
.text-center {
text-align: center;
}
.mb-0 {
margin-bottom: 0;
}
.text-muted {
color: #6c757d;
}
/* Component-specific styles */
</style>

View File

@ -0,0 +1,501 @@
<template>
<div class="container-fluid py-4">
<!-- Header -->
<!-- Form -->
<div class="row">
<div class="col-12">
<div class="card mt-4">
<div class="card-header pb-0">
<div class="d-flex align-items-center">
<p class="font-weight-bold mb-0">
Informations du Thanatopractitioner
</p>
</div>
</div>
<div class="card-body">
<!-- Success Message -->
<div v-if="success" class="alert alert-success" role="alert">
<strong>Succès!</strong> Le thanatopractitioner a été créé avec
succès. Redirection en cours...
</div>
<!-- Loading State -->
<div v-if="loading" class="text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Création en cours...</span>
</div>
<p class="mt-2">Création du thanatopractitioner...</p>
</div>
<!-- Form -->
<form v-else novalidate @submit.prevent="handleSubmit">
<!-- Employee Selection -->
<div class="row">
<div class="col-md-12">
<div class="form-group">
<label for="employee_id" class="form-label"
>Employé *</label
>
<!-- Search Input (only when no employee is selected) -->
<div v-if="!selectedEmployee" class="input-group">
<input
id="employee_search"
v-model="employeeSearch"
type="text"
class="form-control"
placeholder="Rechercher un employé par nom..."
:class="{ 'is-invalid': validationErrors.employee_id }"
/>
<span class="input-group-text">
<i class="fas fa-search"></i>
</span>
</div>
<div
v-if="validationErrors.employee_id && !selectedEmployee"
class="invalid-feedback d-block"
>
{{ validationErrors.employee_id[0] }}
</div>
<!-- Employee Selection Dropdown (only when no employee is selected) -->
<div
v-if="filteredEmployees.length > 0 && !selectedEmployee"
class="dropdown-menu show w-100 mt-1"
style="max-height: 200px; overflow-y: auto"
>
<button
v-for="employee in filteredEmployees"
:key="employee.id"
type="button"
class="dropdown-item"
:class="{
active:
selectedEmployee &&
selectedEmployee.id === employee.id,
}"
@click="selectEmployee(employee)"
>
<div class="d-flex align-items-center">
<soft-avatar
:img="getRandomAvatar()"
size="xs"
class="me-2"
alt="user image"
circular
/>
<div>
<div class="fw-bold">{{ employee.full_name }}</div>
<small class="text-muted"
>{{ employee.email || "Aucun email" }}
{{ employee.job_title || "Aucun poste" }}</small
>
</div>
</div>
</button>
</div>
<!-- Selected Employee Display (only when an employee is selected) -->
<div
v-if="selectedEmployee"
class="mt-2 p-2 border rounded bg-light"
>
<div
class="d-flex align-items-center justify-content-between"
>
<div class="d-flex align-items-center">
<soft-avatar
:img="getRandomAvatar()"
size="xs"
class="me-2"
alt="user image"
circular
/>
<div>
<div class="fw-bold">
{{ selectedEmployee.full_name }}
</div>
<small class="text-muted"
>{{ selectedEmployee.email || "Aucun email" }}
{{
selectedEmployee.job_title || "Aucun poste"
}}</small
>
</div>
</div>
<button
type="button"
class="btn btn-sm btn-outline-danger"
@click="clearEmployeeSelection"
>
<i class="fas fa-times"></i>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Diploma Information -->
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="diploma_number" class="form-label"
>Numéro de Diplôme</label
>
<soft-input
id="diploma_number"
v-model="form.diploma_number"
type="text"
:class="{ 'is-invalid': validationErrors.diploma_number }"
placeholder="Entrez le numéro de diplôme"
/>
<div
v-if="validationErrors.diploma_number"
class="invalid-feedback"
>
{{ validationErrors.diploma_number[0] }}
</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="diploma_date" class="form-label"
>Date du Diplôme</label
>
<soft-input
id="diploma_date"
v-model="form.diploma_date"
type="date"
:class="{ 'is-invalid': validationErrors.diploma_date }"
:max="new Date().toISOString().split('T')[0]"
/>
<div
v-if="validationErrors.diploma_date"
class="invalid-feedback"
>
{{ validationErrors.diploma_date[0] }}
</div>
</div>
</div>
</div>
<!-- Authorization Information -->
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="authorization_number" class="form-label"
>Numéro d'Autorisation</label
>
<soft-input
id="authorization_number"
v-model="form.authorization_number"
type="text"
:class="{
'is-invalid': validationErrors.authorization_number,
}"
placeholder="Entrez le numéro d'autorisation"
/>
<div
v-if="validationErrors.authorization_number"
class="invalid-feedback"
>
{{ validationErrors.authorization_number[0] }}
</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="authorization_issue_date" class="form-label"
>Date d'Émission</label
>
<soft-input
id="authorization_issue_date"
v-model="form.authorization_issue_date"
type="date"
:class="{
'is-invalid': validationErrors.authorization_issue_date,
}"
:max="new Date().toISOString().split('T')[0]"
/>
<div
v-if="validationErrors.authorization_issue_date"
class="invalid-feedback"
>
{{ validationErrors.authorization_issue_date[0] }}
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="authorization_expiry_date" class="form-label"
>Date d'Expiration</label
>
<soft-input
id="authorization_expiry_date"
v-model="form.authorization_expiry_date"
type="date"
:class="{
'is-invalid':
validationErrors.authorization_expiry_date,
}"
:min="new Date().toISOString().split('T')[0]"
/>
<div
v-if="validationErrors.authorization_expiry_date"
class="invalid-feedback"
>
{{ validationErrors.authorization_expiry_date[0] }}
</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="active" class="form-label">Statut</label>
<select
id="active"
v-model="form.active"
class="form-control"
:class="{ 'is-invalid': validationErrors.active }"
>
<option value="true">Actif</option>
<option value="false">Inactif</option>
</select>
<div
v-if="validationErrors.active"
class="invalid-feedback"
>
{{ validationErrors.active[0] }}
</div>
</div>
</div>
</div>
<!-- Notes -->
<div class="row">
<div class="col-md-12">
<div class="form-group">
<label for="notes" class="form-label">Notes</label>
<textarea
id="notes"
v-model="form.notes"
class="form-control"
:class="{ 'is-invalid': validationErrors.notes }"
rows="3"
placeholder="Notes additionnelles..."
></textarea>
<div v-if="validationErrors.notes" class="invalid-feedback">
{{ validationErrors.notes[0] }}
</div>
</div>
</div>
</div>
<!-- Form Actions -->
<div class="row mt-4">
<div class="col-12 d-flex justify-content-end">
<soft-button
type="soft-button"
class="btn btn-light me-3"
@click="$router.go(-1)"
>
Annuler
</soft-button>
<soft-button
type="submit"
class="btn bg-gradient-success"
:disabled="loading"
>
<span
v-if="loading"
class="spinner-border spinner-border-sm me-2"
role="status"
></span>
Créer le Thanatopractitioner
</soft-button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import SoftInput from "@/components/SoftInput.vue";
import SoftButton from "@/components/SoftButton.vue";
import SoftAvatar from "@/components/SoftAvatar.vue";
import { ref, reactive, computed, defineProps, defineEmits } from "vue";
// Sample avatar images
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];
// Props
const props = defineProps({
employees: {
type: Array,
default: () => [],
},
loading: {
type: Boolean,
default: false,
},
validationErrors: {
type: Object,
default: () => ({}),
},
success: {
type: Boolean,
default: false,
},
});
// Emits
const emit = defineEmits(["createThanatopractitioner"]);
// Reactive data
const employeeSearch = ref("");
const selectedEmployee = ref(null);
// Form data
const form = reactive({
employee_id: "",
diploma_number: "",
diploma_date: new Date().toISOString().split("T")[0],
authorization_number: "",
authorization_issue_date: new Date().toISOString().split("T")[0],
authorization_expiry_date: "",
notes: "",
});
// Computed
const filteredEmployees = computed(() => {
if (!employeeSearch.value.trim()) {
return props.employees.slice(0, 10); // Show first 10 employees by default
}
const search = employeeSearch.value.toLowerCase();
return props.employees
.filter(
(employee) =>
employee.full_name.toLowerCase().includes(search) ||
employee.email?.toLowerCase().includes(search) ||
employee.job_title?.toLowerCase().includes(search)
)
.slice(0, 10); // Limit to 10 results
});
// Methods
const getRandomAvatar = () => {
const randomIndex = Math.floor(Math.random() * avatarImages.length);
return avatarImages[randomIndex];
};
const selectEmployee = (employee) => {
selectedEmployee.value = employee;
form.employee_id = employee.id;
employeeSearch.value = "";
};
const clearEmployeeSelection = () => {
selectedEmployee.value = null;
form.employee_id = "";
employeeSearch.value = "";
};
const handleSubmit = () => {
// Clean up the form data
const formData = { ...form };
// Convert empty strings to null for optional fields
Object.keys(formData).forEach((key) => {
if (formData[key] === "") {
formData[key] = null;
}
});
// Ensure employee_id is a number
if (formData.employee_id) {
formData.employee_id = parseInt(formData.employee_id);
}
emit("createThanatopractitioner", formData);
};
</script>
<style scoped>
.form-label {
font-weight: 600;
color: #495057;
margin-bottom: 0.5rem;
}
.invalid-feedback {
display: block;
}
.card {
border: none;
box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.075);
}
.form-control:focus {
border-color: #80bdff;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}
.alert {
border: none;
border-radius: 0.5rem;
}
.dropdown-menu {
border: 1px solid #dee2e6;
border-radius: 0.375rem;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}
.dropdown-item {
padding: 0.5rem 1rem;
}
.dropdown-item:hover {
background-color: #f8f9fa;
}
.dropdown-item.active {
background-color: #007bff;
color: white;
}
.input-group-text {
background-color: #f8f9fa;
border: 1px solid #ced4da;
color: #6c757d;
}
.input-group .form-control.is-invalid {
border-right: 1px solid #dc3545;
}
.border {
border: 1px solid #dee2e6 !important;
}
</style>

View File

@ -0,0 +1,92 @@
<template>
<thanatopractitioner-template>
<template #thanatopractitioner-new-action>
<add-button text="Ajouter" @click="goToThanatopractitioner" />
</template>
<template #select-filter>
<filter-table />
</template>
<template #thanatopractitioner-other-action>
<table-action />
</template>
<template #thanatopractitioner-table>
<thanatopractitioner-table
:data="thanatopractitionerData"
:loading="loadingData"
:pagination="pagination"
@view="goToDetails"
@delete="deleteThanatopractitioner"
@changePage="handleChangePage"
/>
</template>
</thanatopractitioner-template>
</template>
<script setup>
import ThanatopractitionerTemplate from "@/components/templates/CRM/ThanatopractitionerTemplate.vue";
import ThanatopractitionerTable from "@/components/molecules/Thanatopractitioners/ThanatopractitionerTable.vue";
import AddButton from "@/components/molecules/new-button/addButton.vue";
import FilterTable from "@/components/molecules/Tables/FilterTable.vue";
import TableAction from "@/components/molecules/Tables/TableAction.vue";
import { defineProps, defineEmits } from "vue";
import { useRouter } from "vue-router";
const router = useRouter();
const emit = defineEmits([
"pushDetails",
"deleteThanatopractitioner",
"changePage",
]);
const props = defineProps({
thanatopractitionerData: {
type: Array,
default: [],
},
loadingData: {
type: Boolean,
default: false,
},
pagination: {
type: Object,
default: null,
},
});
const goToThanatopractitioner = () => {
router.push({
name: "Creation thanatopractitioner",
});
};
const goToDetails = (thanatopractitionerId) => {
emit("pushDetails", thanatopractitionerId);
};
const deleteThanatopractitioner = (thanatopractitionerId) => {
console.log(
"deleteThanatopractitioner called in ThanatopractitionerPresentation with ID:",
thanatopractitionerId
);
emit("deleteThanatopractitioner", thanatopractitionerId);
};
const handleChangePage = (page) => {
console.log(
"handleChangePage called in ThanatopractitionerPresentation with page:",
page
);
if (page >= 1 && page <= (props.pagination?.last_page || 1)) {
console.log(
"Emitting changePage event from ThanatopractitionerPresentation:",
page
);
emit("changePage", page);
}
};
</script>
<style scoped>
/* Component-specific styles */
</style>

View File

@ -78,133 +78,192 @@
</div>
<!-- Data State -->
<div v-else class="table-responsive">
<table id="employee-list" class="table table-flush">
<thead class="thead-light">
<tr>
<th>ID</th>
<th>Nom & Prénom</th>
<th>Email</th>
<th>Téléphone</th>
<th>Poste</th>
<th>Date d'embauche</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr v-for="employee in data" :key="employee.id">
<!-- ID Column -->
<td>
<div class="d-flex align-items-center">
<soft-checkbox />
<p class="text-xs font-weight-bold ms-2 mb-0">
{{ employee.id }}
</p>
</div>
</td>
<div v-else>
<div class="table-responsive">
<table id="employee-list" class="table table-flush">
<thead class="thead-light">
<tr>
<th>ID</th>
<th>Nom & Prénom</th>
<th>Email</th>
<th>Téléphone</th>
<th>Poste</th>
<th>Date d'embauche</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr v-for="employee in data" :key="employee.id">
<!-- ID Column -->
<td>
<div class="d-flex align-items-center">
<soft-checkbox />
<p class="text-xs font-weight-bold ms-2 mb-0">
{{ employee.id }}
</p>
</div>
</td>
<!-- Name Column -->
<td class="font-weight-bold">
<div class="d-flex align-items-center">
<soft-avatar
:img="getRandomAvatar()"
size="xs"
class="me-2"
alt="user image"
circular
/>
<div>
<span
>{{ employee.last_name }} {{ employee.first_name }}</span
>
<div
v-if="employee.thanatopractitioner"
class="text-xs text-info"
>
Thanatopractitioner
<!-- Name Column -->
<td class="font-weight-bold">
<div class="d-flex align-items-center">
<soft-avatar
:img="getRandomAvatar()"
size="xs"
class="me-2"
alt="user image"
circular
/>
<div>
<span
>{{ employee.last_name }} {{ employee.first_name }}</span
>
<div
v-if="employee.thanatopractitioner"
class="text-xs text-info"
>
Thanatopractitioner
</div>
</div>
</div>
</div>
</td>
</td>
<!-- Email Column -->
<td class="text-xs font-weight-bold">
<span class="text-xs">{{ employee.email || "N/A" }}</span>
</td>
<!-- Email Column -->
<td class="text-xs font-weight-bold">
<span class="text-xs">{{ employee.email || "N/A" }}</span>
</td>
<!-- Phone Column -->
<td class="text-xs font-weight-bold">
<span class="text-xs">{{ employee.phone || "N/A" }}</span>
</td>
<!-- Phone Column -->
<td class="text-xs font-weight-bold">
<span class="text-xs">{{ employee.phone || "N/A" }}</span>
</td>
<!-- Position Column -->
<td class="text-xs font-weight-bold">
<div class="d-flex align-items-center">
<soft-button
:color="getPositionColor(employee.job_title)"
variant="outline"
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
>
<i
:class="getPositionIcon(employee.job_title)"
aria-hidden="true"
></i>
</soft-button>
<span>{{ employee.job_title || "N/A" }}</span>
</div>
</td>
<!-- Position Column -->
<td class="text-xs font-weight-bold">
<div class="d-flex align-items-center">
<soft-button
:color="getPositionColor(employee.job_title)"
variant="outline"
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
>
<i
:class="getPositionIcon(employee.job_title)"
aria-hidden="true"
></i>
</soft-button>
<span>{{ employee.job_title || "N/A" }}</span>
</div>
</td>
<!-- Hire Date Column -->
<td class="text-xs font-weight-bold">
<span class="text-xs">{{ formatDate(employee.hire_date) }}</span>
</td>
<!-- Hire Date Column -->
<td class="text-xs font-weight-bold">
<span class="text-xs">{{
formatDate(employee.hire_date)
}}</span>
</td>
<!-- Status Column -->
<td class="text-xs font-weight-bold">
<div class="d-flex align-items-center">
<soft-button
:color="employee.active ? 'success' : 'danger'"
variant="outline"
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
>
<i
:class="employee.active ? 'fas fa-check' : 'fas fa-times'"
aria-hidden="true"
></i>
</soft-button>
<span>{{ employee.active ? "Actif" : "Inactif" }}</span>
</div>
</td>
<!-- Status Column -->
<td class="text-xs font-weight-bold">
<div class="d-flex align-items-center">
<soft-button
:color="employee.active ? 'success' : 'danger'"
variant="outline"
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
>
<i
:class="employee.active ? 'fas fa-check' : 'fas fa-times'"
aria-hidden="true"
></i>
</soft-button>
<span>{{ employee.active ? "Actif" : "Inactif" }}</span>
</div>
</td>
<td>
<div class="d-flex align-items-center gap-2">
<!-- View Button -->
<soft-button
color="info"
variant="outline"
title="Voir l'employé"
:data-employee-id="employee.id"
class="btn-icon-only btn-rounded mb-0 btn-sm d-flex align-items-center justify-content-center"
>
<i class="fas fa-eye" aria-hidden="true"></i>
</soft-button>
<td>
<div class="d-flex align-items-center gap-2">
<!-- View Button -->
<soft-button
color="info"
variant="outline"
title="Voir l'employé"
:data-employee-id="employee.id"
class="btn-icon-only btn-rounded mb-0 btn-sm d-flex align-items-center justify-content-center"
@click="handleView(employee.id)"
>
<i class="fas fa-eye" aria-hidden="true"></i>
</soft-button>
<!-- Delete Button -->
<soft-button
color="danger"
variant="outline"
title="Supprimer l'employé"
:data-employee-id="employee.id"
class="btn-icon-only btn-rounded mb-0 btn-sm d-flex align-items-center justify-content-center"
>
<i class="fas fa-trash" aria-hidden="true"></i>
</soft-button>
</div>
</td>
</tr>
</tbody>
</table>
<!-- Delete Button -->
<soft-button
color="danger"
variant="outline"
title="Supprimer l'employé"
:data-employee-id="employee.id"
class="btn-icon-only btn-rounded mb-0 btn-sm d-flex align-items-center justify-content-center"
@click="handleDelete(employee.id)"
>
<i class="fas fa-trash" aria-hidden="true"></i>
</soft-button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Custom Pagination Controls -->
<div
v-if="pagination.total > pagination.per_page"
class="d-flex justify-content-between align-items-center mt-3"
>
<div class="text-sm text-muted">
Affichage de {{ pagination.from }} à {{ pagination.to }} sur
{{ pagination.total }} employés
</div>
<div class="d-flex align-items-center gap-2">
<!-- Previous Button -->
<soft-button
color="outline"
variant="outline"
class="btn-sm"
:disabled="pagination.current_page === 1 || loading"
@click="changePage(pagination.current_page - 1)"
>
<i class="fas fa-chevron-left me-1"></i>
Précédent
</soft-button>
<!-- Page Numbers -->
<div class="d-flex gap-1">
<soft-button
v-for="page in getVisiblePages()"
:key="page"
:color="page === pagination.current_page ? 'primary' : 'outline'"
variant="outline"
class="btn-sm"
:disabled="loading"
@click="changePage(page)"
>
{{ page }}
</soft-button>
</div>
<!-- Next Button -->
<soft-button
color="outline"
variant="outline"
class="btn-sm"
:disabled="
pagination.current_page === pagination.last_page || loading
"
@click="changePage(pagination.current_page + 1)"
>
Suivant
<i class="fas fa-chevron-right ms-1"></i>
</soft-button>
</div>
</div>
</div>
<!-- Empty State -->
@ -222,13 +281,13 @@
<script setup>
import { ref, onMounted, watch, onUnmounted } from "vue";
import { DataTable } from "simple-datatables";
// import { DataTable } from "simple-datatables"; // Disabled to avoid interference
import SoftCheckbox from "@/components/SoftCheckbox.vue";
import SoftButton from "@/components/SoftButton.vue";
import SoftAvatar from "@/components/SoftAvatar.vue";
import { defineProps, defineEmits } from "vue";
const emit = defineEmits(["view", "delete"]);
const emit = defineEmits(["view", "delete", "changePage"]);
// Sample avatar images
import img1 from "@/assets/img/team-2.jpg";
@ -240,8 +299,8 @@ import img6 from "@/assets/img/ivana-squares.jpg";
const avatarImages = [img1, img2, img3, img4, img5, img6];
// Reactive data
const dataTableInstance = ref(null);
// Reactive data - DataTable disabled
// const dataTableInstance = ref(null);
const props = defineProps({
data: {
@ -256,6 +315,17 @@ const props = defineProps({
type: Number,
default: 5,
},
pagination: {
type: Object,
default: () => ({
current_page: 1,
last_page: 1,
per_page: 10,
total: 0,
from: 0,
to: 0,
}),
},
});
// Methods
@ -292,6 +362,67 @@ const getPositionIcon = (position) => {
return icons[position] || "fas fa-user";
};
// Direct button handlers
const handleView = (employeeId) => {
console.log("Direct view button clicked for ID:", employeeId);
emit("view", employeeId);
};
const handleDelete = (employeeId) => {
console.log("Direct delete button clicked for ID:", employeeId);
emit("delete", employeeId);
};
// Pagination methods
const changePage = (page) => {
console.log("changePage called in EmployeeTable with page:", page);
if (page >= 1 && page <= props.pagination.last_page) {
console.log("Emitting changePage event from EmployeeTable:", page);
emit("changePage", page);
}
};
const getVisiblePages = () => {
const current = props.pagination.current_page;
const last = props.pagination.last_page;
const pages = [];
if (last <= 7) {
// Show all pages if 7 or fewer
for (let i = 1; i <= last; i++) {
pages.push(i);
}
} else {
// Show first page, current range, and last page
pages.push(1);
if (current > 3) {
pages.push("...");
}
const start = Math.max(2, current - 1);
const end = Math.min(last - 1, current + 1);
for (let i = start; i <= end; i++) {
if (!pages.includes(i)) {
pages.push(i);
}
}
if (current < last - 2) {
pages.push("...");
}
if (!pages.includes(last)) {
pages.push(last);
}
}
return pages;
};
// Commented out DataTable initialization
/*
const initializeDataTable = () => {
// Destroy existing instance if it exists
if (dataTableInstance.value) {
@ -301,62 +432,75 @@ const initializeDataTable = () => {
const dataTableEl = document.getElementById("employee-list");
if (dataTableEl) {
// Initialize DataTable with search and default pagination
dataTableInstance.value = new DataTable(dataTableEl, {
searchable: true,
fixedHeight: true,
perPage: 15,
perPageSelect: [5, 10, 15, 20, 25],
perPage: 10, // Default to 10 entries per page
perPageSelect: false, // Disable per-page selector since we handle it server-side
pager: false, // Disable DataTable pagination since we handle it server-side
});
// Add click listener for action buttons
dataTableEl.addEventListener("click", handleTableClick);
}
};
const handleTableClick = (event) => {
console.log("Table click detected:", event.target);
const button = event.target.closest("button");
if (!button) return;
const employeeId = button.getAttribute("data-employee-id");
console.log("Employee ID:", employeeId);
console.log("Button title:", button.title);
if (
button.title === "Supprimer l'employé" ||
button.querySelector(".fa-trash")
) {
console.log("Delete button clicked!");
emit("delete", employeeId);
} else if (
button.title === "Voir l'employé" ||
button.querySelector(".fa-eye")
) {
console.log("View button clicked!");
emit("view", employeeId);
}
};
*/
// Watch for data changes to reinitialize datatable
// Watch for data changes
watch(
() => props.data,
() => {
if (!props.loading) {
// Small delay to ensure DOM is updated
setTimeout(() => {
initializeDataTable();
}, 100);
console.log("EmployeeTable: Data changed");
}
},
{ deep: true }
);
onUnmounted(() => {
const dataTableEl = document.getElementById("employee-list");
if (dataTableEl) {
dataTableEl.removeEventListener("click", handleTableClick);
}
if (dataTableInstance.value) {
dataTableInstance.value.destroy();
}
// Clean up any event listeners if needed
// const dataTableEl = document.getElementById("employee-list");
// if (dataTableEl) {
// dataTableEl.removeEventListener("click", handleTableClick);
// }
// if (dataTableInstance.value) {
// dataTableInstance.value.destroy();
// }
});
// Initialize data
onMounted(() => {
if (!props.loading && props.data.length > 0) {
initializeDataTable();
console.log(
"EmployeeTable: Component mounted with",
props.data.length,
"employees"
);
}
});
</script>

View File

@ -0,0 +1,632 @@
<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>ID</th>
<th>Nom & Prénom</th>
<th>Email</th>
<th>Téléphone</th>
<th>Numéro diplôme</th>
<th>Numéro autorisation</th>
<th>Validité autorisation</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr v-for="i in skeletonRows" :key="i" class="skeleton-row">
<!-- ID Column Skeleton -->
<td>
<div class="d-flex align-items-center">
<div class="skeleton-checkbox"></div>
<div class="skeleton-text short 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 medium ms-2"></div>
</div>
</td>
<!-- Email Column Skeleton -->
<td>
<div class="skeleton-text long"></div>
</td>
<!-- Phone Column Skeleton -->
<td>
<div class="skeleton-text medium"></div>
</td>
<!-- Diploma Number Column Skeleton -->
<td>
<div class="skeleton-text medium"></div>
</td>
<!-- Authorization Number Column Skeleton -->
<td>
<div class="skeleton-text medium"></div>
</td>
<!-- Authorization Expiry Column Skeleton -->
<td>
<div class="skeleton-text medium"></div>
</td>
<!-- Status Column Skeleton -->
<td>
<div class="d-flex align-items-center">
<div class="skeleton-icon small"></div>
<div class="skeleton-text short ms-2"></div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Data State -->
<div v-else>
<div class="table-responsive">
<table id="thanatopractitioner-list" class="table table-flush">
<thead class="thead-light">
<tr>
<th>ID</th>
<th>Nom & Prénom</th>
<th>Email</th>
<th>Téléphone</th>
<th>Numéro diplôme</th>
<th>Numéro autorisation</th>
<th>Validité autorisation</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr
v-for="thanatopractitioner in data"
:key="thanatopractitioner.id"
>
<!-- ID Column -->
<td>
<div class="d-flex align-items-center">
<soft-checkbox />
<p class="text-xs font-weight-bold ms-2 mb-0">
{{ thanatopractitioner.id }}
</p>
</div>
</td>
<!-- Name Column (from employee relation) -->
<td class="font-weight-bold">
<div class="d-flex align-items-center">
<soft-avatar
:img="getRandomAvatar()"
size="xs"
class="me-2"
alt="user image"
circular
/>
<div>
<span>{{
thanatopractitioner.employee?.full_name ||
`${thanatopractitioner.employee?.first_name || ""} ${
thanatopractitioner.employee?.last_name || ""
}` ||
"N/A"
}}</span>
<div class="text-xs text-info">Thanatopractitioner</div>
</div>
</div>
</td>
<!-- Email Column (from employee relation) -->
<td class="text-xs font-weight-bold">
<span class="text-xs">{{
thanatopractitioner.employee?.email || "N/A"
}}</span>
</td>
<!-- Phone Column (from employee relation) -->
<td class="text-xs font-weight-bold">
<span class="text-xs">{{
thanatopractitioner.employee?.phone || "N/A"
}}</span>
</td>
<!-- Diploma Number Column -->
<td class="text-xs font-weight-bold">
<div class="d-flex align-items-center">
<soft-button
color="primary"
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-graduation-cap" aria-hidden="true"></i>
</soft-button>
<span>{{ thanatopractitioner.diploma_number || "N/A" }}</span>
</div>
</td>
<!-- Authorization Number Column -->
<td class="text-xs font-weight-bold">
<div 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-id-card" aria-hidden="true"></i>
</soft-button>
<span>{{
thanatopractitioner.authorization_number || "N/A"
}}</span>
</div>
</td>
<!-- Authorization Expiry Column -->
<td class="text-xs font-weight-bold">
<div class="d-flex align-items-center">
<div
:class="[
'badge',
'badge-sm',
isAuthorizationValid(
thanatopractitioner.authorization_expiry_date
)
? 'bg-success'
: 'bg-danger',
]"
>
<i
:class="[
'fas',
isAuthorizationValid(
thanatopractitioner.authorization_expiry_date
)
? 'fa-check-circle'
: 'fa-times-circle',
'me-1',
]"
></i>
{{
formatDate(thanatopractitioner.authorization_expiry_date)
}}
</div>
</div>
</td>
<!-- Status Column -->
<td class="text-xs font-weight-bold">
<div class="d-flex align-items-center">
<soft-button
:color="thanatopractitioner.active ? 'success' : 'danger'"
variant="outline"
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
>
<i
:class="
thanatopractitioner.active
? 'fas fa-check'
: 'fas fa-times'
"
aria-hidden="true"
></i>
</soft-button>
<span>{{
thanatopractitioner.active ? "Actif" : "Inactif"
}}</span>
</div>
</td>
<td>
<div class="d-flex align-items-center gap-2">
<!-- View Button -->
<soft-button
color="info"
variant="outline"
title="Voir le thanatopractitioner"
:data-thanatopractitioner-id="thanatopractitioner.id"
class="btn-icon-only btn-rounded mb-0 btn-sm d-flex align-items-center justify-content-center"
@click="handleView(thanatopractitioner.id)"
>
<i class="fas fa-eye" aria-hidden="true"></i>
</soft-button>
<!-- Delete Button -->
<soft-button
color="danger"
variant="outline"
title="Supprimer le thanatopractitioner"
:data-thanatopractitioner-id="thanatopractitioner.id"
class="btn-icon-only btn-rounded mb-0 btn-sm d-flex align-items-center justify-content-center"
@click="handleDelete(thanatopractitioner.id)"
>
<i class="fas fa-trash" aria-hidden="true"></i>
</soft-button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Custom Pagination Controls -->
<div
v-if="pagination.total > pagination.per_page"
class="d-flex justify-content-between align-items-center mt-3"
>
<div class="text-sm text-muted">
Affichage de {{ pagination.from }} à {{ pagination.to }} sur
{{ pagination.total }} thanatopractitioners
</div>
<div class="d-flex align-items-center gap-2">
<!-- Previous Button -->
<soft-button
color="outline"
variant="outline"
class="btn-sm"
:disabled="pagination.current_page === 1 || loading"
@click="changePage(pagination.current_page - 1)"
>
<i class="fas fa-chevron-left me-1"></i>
Précédent
</soft-button>
<!-- Page Numbers -->
<div class="d-flex gap-1">
<soft-button
v-for="page in getVisiblePages()"
:key="page"
:color="page === pagination.current_page ? 'primary' : 'outline'"
variant="outline"
class="btn-sm"
:disabled="loading"
@click="changePage(page)"
>
{{ page }}
</soft-button>
</div>
<!-- Next Button -->
<soft-button
color="outline"
variant="outline"
class="btn-sm"
:disabled="
pagination.current_page === pagination.last_page || loading
"
@click="changePage(pagination.current_page + 1)"
>
Suivant
<i class="fas fa-chevron-right ms-1"></i>
</soft-button>
</div>
</div>
</div>
<!-- Empty State -->
<div v-if="!loading && data.length === 0" class="empty-state">
<div class="empty-icon">
<i class="fas fa-user-md fa-3x text-muted"></i>
</div>
<h5 class="empty-title">Aucun thanatopractitioner trouvé</h5>
<p class="empty-text text-muted">
Aucun thanatopractitioner à afficher pour le moment.
</p>
</div>
</div>
</template>
<script setup>
import { ref, watch, onUnmounted, onMounted } from "vue";
import SoftCheckbox from "@/components/SoftCheckbox.vue";
import SoftButton from "@/components/SoftButton.vue";
import SoftAvatar from "@/components/SoftAvatar.vue";
import { defineProps, defineEmits } from "vue";
const emit = defineEmits(["view", "delete", "changePage"]);
// Sample avatar images
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];
const props = defineProps({
data: {
type: Array,
default: [],
},
loading: {
type: Boolean,
default: false,
},
skeletonRows: {
type: Number,
default: 5,
},
pagination: {
type: Object,
default: () => ({
current_page: 1,
last_page: 1,
per_page: 10,
total: 0,
from: 0,
to: 0,
}),
},
});
// Methods
const getRandomAvatar = () => {
const randomIndex = Math.floor(Math.random() * avatarImages.length);
return avatarImages[randomIndex];
};
const formatDate = (dateString) => {
if (!dateString) return "N/A";
const date = new Date(dateString);
return date.toLocaleDateString("fr-FR");
};
const isAuthorizationValid = (expiryDate) => {
if (!expiryDate) return false;
const validityDate = new Date(expiryDate);
const today = new Date();
return validityDate > today;
};
// Direct button handlers
const handleView = (thanatopractitionerId) => {
console.log("Direct view button clicked for ID:", thanatopractitionerId);
emit("view", thanatopractitionerId);
};
const handleDelete = (thanatopractitionerId) => {
console.log("Direct delete button clicked for ID:", thanatopractitionerId);
emit("delete", thanatopractitionerId);
};
// Pagination methods
const changePage = (page) => {
console.log("changePage called in ThanatopractitionerTable with page:", page);
if (page >= 1 && page <= props.pagination.last_page) {
console.log(
"Emitting changePage event from ThanatopractitionerTable:",
page
);
emit("changePage", page);
}
};
const getVisiblePages = () => {
const current = props.pagination.current_page;
const last = props.pagination.last_page;
const pages = [];
if (last <= 7) {
// Show all pages if 7 or fewer
for (let i = 1; i <= last; i++) {
pages.push(i);
}
} else {
// Show first page, current range, and last page
pages.push(1);
if (current > 3) {
pages.push("...");
}
const start = Math.max(2, current - 1);
const end = Math.min(last - 1, current + 1);
for (let i = start; i <= end; i++) {
if (!pages.includes(i)) {
pages.push(i);
}
}
if (current < last - 2) {
pages.push("...");
}
if (!pages.includes(last)) {
pages.push(last);
}
}
return pages;
};
// Watch for data changes
watch(
() => props.data,
() => {
if (!props.loading) {
console.log("ThanatopractitionerTable: Data changed");
}
},
{ deep: true }
);
onUnmounted(() => {
// Clean up any event listeners if needed
});
// Initialize data
onMounted(() => {
if (!props.loading && props.data.length > 0) {
console.log(
"ThanatopractitionerTable: Component mounted with",
props.data.length,
"thanatopractitioners"
);
}
});
</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-checkbox {
width: 18px;
height: 18px;
border-radius: 3px;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 2s 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-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;
}
.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;
}
.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;
}
.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.long {
width: 80px;
}
.skeleton-text.medium {
width: 60px;
}
}
</style>

View File

@ -0,0 +1,431 @@
<template>
<!-- Modal Component -->
<div
class="modal fade"
:class="{ show: isVisible, 'd-block': isVisible }"
tabindex="-1"
role="dialog"
:aria-hidden="!isVisible"
>
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<!-- Header -->
<div class="modal-header" :class="headerClass">
<h5 class="modal-title">
<i :class="[iconClass, 'me-2']"></i>
{{ title }}
</h5>
<button
type="button"
class="btn-close"
:class="closeButtonClass"
aria-label="Close"
@click="handleCancel"
></button>
</div>
<!-- Body -->
<div class="modal-body">
<div class="d-flex align-items-center">
<div class="me-3" :class="iconContainerClass">
<i :class="[iconClass, 'fa-lg']"></i>
</div>
<div class="flex-grow-1">
<p class="mb-0" :class="messageClass">{{ message }}</p>
<small v-if="details" class="text-muted d-block mt-1">
{{ details }}
</small>
</div>
</div>
</div>
<!-- Footer -->
<div class="modal-footer">
<button
type="button"
class="btn"
:class="cancelButtonClass"
:disabled="isLoading"
@click="handleCancel"
>
<i :class="cancelIconClass" class="me-1"></i>
{{ cancelText }}
</button>
<button
type="button"
class="btn"
:class="confirmButtonClass"
:disabled="isLoading"
@click="handleConfirm"
>
<span
v-if="isLoading"
class="spinner-border spinner-border-sm me-2"
role="status"
></span>
<i v-else :class="confirmIconClass" class="me-1"></i>
{{ confirmText }}
</button>
</div>
</div>
</div>
</div>
<!-- Backdrop -->
<div
v-if="isVisible"
class="modal-backdrop fade show"
@click="handleCancel"
></div>
</template>
<script setup>
import { ref, reactive, watch, onMounted, onUnmounted, computed } from "vue";
import { defineProps, defineEmits } from "vue";
// Props
const props = defineProps({
isVisible: {
type: Boolean,
default: false,
},
title: {
type: String,
default: "Confirmation",
},
message: {
type: String,
required: true,
},
details: {
type: String,
default: "",
},
type: {
type: String,
default: "danger", // danger, warning, info, success
validator: (value) =>
["danger", "warning", "info", "success"].includes(value),
},
confirmText: {
type: String,
default: "Confirmer",
},
cancelText: {
type: String,
default: "Annuler",
},
isLoading: {
type: Boolean,
default: false,
},
showCancel: {
type: Boolean,
default: true,
},
persistent: {
type: Boolean,
default: false, // If true, clicking backdrop won't close
},
});
// Emits
const emit = defineEmits(["confirm", "cancel", "close"]);
// Computed classes based on type
const headerClass = computed(() => {
const base = "border-bottom-0";
switch (props.type) {
case "danger":
return `${base} bg-danger text-white`;
case "warning":
return `${base} bg-warning text-dark`;
case "info":
return `${base} bg-info text-white`;
case "success":
return `${base} bg-success text-white`;
default:
return `${base} bg-light`;
}
});
const closeButtonClass = computed(() => {
switch (props.type) {
case "danger":
case "info":
case "success":
return "btn-close-white";
case "warning":
return "btn-close-dark";
default:
return "";
}
});
const iconClass = computed(() => {
switch (props.type) {
case "danger":
return "fas fa-trash-alt text-white";
case "warning":
return "fas fa-exclamation-triangle text-dark";
case "info":
return "fas fa-info-circle text-white";
case "success":
return "fas fa-check-circle text-white";
default:
return "fas fa-question-circle";
}
});
const iconContainerClass = computed(() => {
switch (props.type) {
case "danger":
return "d-flex align-items-center justify-content-center rounded-circle bg-danger text-white";
case "warning":
return "d-flex align-items-center justify-content-center rounded-circle bg-warning text-dark";
case "info":
return "d-flex align-items-center justify-content-center rounded-circle bg-info text-white";
case "success":
return "d-flex align-items-center justify-content-center rounded-circle bg-success text-white";
default:
return "d-flex align-items-center justify-content-center rounded-circle bg-secondary text-white";
}
});
const messageClass = computed(() => {
switch (props.type) {
case "danger":
return "text-danger fw-bold";
case "warning":
return "text-warning fw-bold";
case "info":
return "text-info fw-bold";
case "success":
return "text-success fw-bold";
default:
return "text-dark";
}
});
const cancelButtonClass = computed(() => {
switch (props.type) {
case "danger":
return "btn-outline-light";
case "warning":
return "btn-outline-dark";
case "info":
return "btn-outline-light";
case "success":
return "btn-outline-light";
default:
return "btn-outline-secondary";
}
});
const confirmButtonClass = computed(() => {
switch (props.type) {
case "danger":
return "btn btn-danger";
case "warning":
return "btn btn-warning text-dark";
case "info":
return "btn btn-info";
case "success":
return "btn btn-success";
default:
return "btn btn-primary";
}
});
const cancelIconClass = computed(() => {
switch (props.type) {
case "danger":
case "info":
case "success":
return "fas fa-times";
case "warning":
return "fas fa-times";
default:
return "fas fa-times";
}
});
const confirmIconClass = computed(() => {
switch (props.type) {
case "danger":
return "fas fa-trash-alt";
case "warning":
return "fas fa-exclamation-triangle";
case "info":
return "fas fa-info-circle";
case "success":
return "fas fa-check";
default:
return "fas fa-check";
}
});
// Methods
const handleConfirm = () => {
emit("confirm");
};
const handleCancel = () => {
if (!props.persistent && !props.isLoading) {
emit("cancel");
emit("close");
}
};
// Keyboard event handler
const handleKeydown = (event) => {
if (event.key === "Escape" && props.isVisible && !props.persistent) {
handleCancel();
}
};
// Add/remove event listeners
onMounted(() => {
document.addEventListener("keydown", handleKeydown);
});
onUnmounted(() => {
document.removeEventListener("keydown", handleKeydown);
});
</script>
<style scoped>
.modal {
background-color: rgba(0, 0, 0, 0.5);
}
.modal-content {
border: none;
border-radius: 0.5rem;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
}
.modal-header {
border-top-left-radius: 0.5rem;
border-top-right-radius: 0.5rem;
}
.modal-body {
padding: 1.5rem;
}
.modal-footer {
border-top: 1px solid #e9ecef;
background-color: #f8f9fa;
border-bottom-left-radius: 0.5rem;
border-bottom-right-radius: 0.5rem;
}
.icon-container {
width: 50px;
height: 50px;
flex-shrink: 0;
}
.btn {
border-radius: 0.375rem;
padding: 0.5rem 1rem;
font-weight: 500;
transition: all 0.2s ease;
}
.btn:hover:not(:disabled) {
transform: translateY(-1px);
}
.btn:disabled {
cursor: not-allowed;
opacity: 0.65;
}
.spinner-border-sm {
width: 1rem;
height: 1rem;
}
.fade {
transition: opacity 0.15s linear;
}
.modal-backdrop {
opacity: 0.5;
}
.bg-danger {
background-color: #dc3545 !important;
}
.bg-warning {
background-color: #ffc107 !important;
}
.bg-info {
background-color: #0dcaf0 !important;
}
.bg-success {
background-color: #198754 !important;
}
.text-danger {
color: #dc3545 !important;
}
.text-warning {
color: #ffc107 !important;
}
.text-info {
color: #0dcaf0 !important;
}
.text-success {
color: #198754 !important;
}
.btn-danger {
background-color: #dc3545;
border-color: #dc3545;
}
.btn-danger:hover:not(:disabled) {
background-color: #c82333;
border-color: #bd2130;
}
.btn-warning {
background-color: #ffc107;
border-color: #ffc107;
color: #212529;
}
.btn-warning:hover:not(:disabled) {
background-color: #e0a800;
border-color: #d39e00;
}
.btn-info {
background-color: #0dcaf0;
border-color: #0dcaf0;
}
.btn-info:hover:not(:disabled) {
background-color: #31d2f2;
border-color: #25c2e3;
}
.btn-success {
background-color: #198754;
border-color: #198754;
}
.btn-success:hover:not(:disabled) {
background-color: #146c43;
border-color: #11543a;
}
</style>

View File

@ -0,0 +1,363 @@
<template>
<div>
<!-- Header -->
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<h6 class="mb-0">Activité de l'employé</h6>
<div>
<button class="btn btn-sm btn-outline-primary">
<i class="fas fa-filter me-1"></i>
Filtrer
</button>
</div>
</div>
</div>
</div>
<!-- Activity Timeline -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<div class="card-head-row">
<div class="card-title">Historique des actions</div>
</div>
</div>
<div class="card-body">
<!-- Placeholder for future activity implementation -->
<div class="alert alert-info" role="alert">
<i class="fas fa-info-circle me-2"></i>
<strong>À venir:</strong> Cette section affichera l'historique
complet des activités de l'employé, incluant les connexions,
modifications, et actions effectuées.
</div>
<!-- Mock Timeline for demonstration -->
<div class="timeline">
<div class="timeline-item">
<div class="timeline-marker bg-success"></div>
<div class="timeline-content">
<h6 class="timeline-title">Compte créé</h6>
<p class="timeline-description">
Le compte employé a été créé dans le système
</p>
<small class="text-muted">{{
formatDate(employee.created_at)
}}</small>
</div>
</div>
<div class="timeline-item">
<div class="timeline-marker bg-primary"></div>
<div class="timeline-content">
<h6 class="timeline-title">Profil mis à jour</h6>
<p class="timeline-description">
Les informations de l'employé ont été mises à jour
</p>
<small class="text-muted">{{
formatDate(employee.updated_at)
}}</small>
</div>
</div>
<div v-if="employee.thanatopractitioner" class="timeline-item">
<div class="timeline-marker bg-warning"></div>
<div class="timeline-content">
<h6 class="timeline-title">
Certification thanatopractitioner
</h6>
<p class="timeline-description">
Le statut de thanatopractitioner a été ajouté
</p>
<small class="text-muted">{{
formatDate(employee.thanatopractitioner.created_at)
}}</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Statistics -->
<div class="row mt-4">
<div class="col-md-3 col-sm-6 mb-4">
<div class="card card-stats card-round">
<div class="card-body">
<div class="row align-items-center">
<div class="col-icon">
<div
class="icon-big text-center icon-success bubble-shadow-small"
>
<i class="fas fa-calendar-check"></i>
</div>
</div>
<div class="col col-stats ms-3 ms-sm-0 mt-3">
<div class="numbers">
<p class="card-category">Jours de service</p>
<h4 class="card-title">{{ getServiceDays() }}</h4>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3 col-sm-6 mb-4">
<div class="card card-stats card-round">
<div class="card-body">
<div class="row align-items-center">
<div class="col-icon">
<div class="icon-big text-center icon-info bubble-shadow-small">
<i class="fas fa-file-alt"></i>
</div>
</div>
<div class="col col-stats ms-3 ms-sm-0 mt-3">
<div class="numbers">
<p class="card-category">Documents</p>
<h4 class="card-title">0</h4>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3 col-sm-6 mb-4">
<div class="card card-stats card-round">
<div class="card-body">
<div class="row align-items-center">
<div class="col-icon">
<div
class="icon-big text-center icon-warning bubble-shadow-small"
>
<i class="fas fa-tasks"></i>
</div>
</div>
<div class="col col-stats ms-3 ms-sm-0 mt-3">
<div class="numbers">
<p class="card-category">Tâches terminées</p>
<h4 class="card-title">0</h4>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3 col-sm-6 mb-4">
<div class="card card-stats card-round">
<div class="card-body">
<div class="row align-items-center">
<div class="col-icon">
<div
class="icon-big text-center icon-primary bubble-shadow-small"
>
<i class="fas fa-chart-line"></i>
</div>
</div>
<div class="col col-stats ms-3 ms-sm-0 mt-3">
<div class="numbers">
<p class="card-category">Performance</p>
<h4 class="card-title">--</h4>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Recent Actions -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<div class="card-head-row">
<div class="card-title">Actions récentes</div>
</div>
</div>
<div class="card-body">
<!-- Placeholder table -->
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Action</th>
<th>Description</th>
<th>Date</th>
<th>Statut</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<span class="badge bg-primary">Création</span>
</td>
<td>Compte employé créé</td>
<td>{{ formatDate(employee.created_at) }}</td>
<td>
<span class="badge bg-success">Terminé</span>
</td>
</tr>
<tr>
<td>
<span class="badge bg-info">Modification</span>
</td>
<td>Profil mis à jour</td>
<td>{{ formatDate(employee.updated_at) }}</td>
<td>
<span class="badge bg-success">Terminé</span>
</td>
</tr>
</tbody>
</table>
</div>
<div class="text-center mt-3">
<button class="btn btn-outline-primary btn-sm">
<i class="fas fa-history me-1"></i>
Voir tout l'historique
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { defineProps } from "vue";
const props = defineProps({
employee: {
type: Object,
required: true,
},
});
const formatDate = (dateString) => {
if (!dateString) return "Non renseignée";
const date = new Date(dateString);
return date.toLocaleDateString("fr-FR", {
year: "numeric",
month: "long",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
});
};
const getServiceDays = () => {
if (!props.employee.hire_date) return "0";
const hireDate = new Date(props.employee.hire_date);
const now = new Date();
const diffTime = Math.abs(now.getTime() - hireDate.getTime());
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
return diffDays.toString();
};
</script>
<style scoped>
.card-stats {
border: none;
box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.075);
border-radius: 0.5rem;
}
.card-title {
font-size: 1rem;
margin-bottom: 0;
}
.card-category {
font-size: 0.875rem;
color: #6c757d;
margin-bottom: 0.25rem;
}
.card {
border: none;
box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.075);
border-radius: 0.5rem;
}
.card-head-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.icon-big {
font-size: 2.5rem;
}
.bubble-shadow-small {
box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.1);
}
.col-icon {
flex: 0 0 auto;
width: 4.5rem;
}
.timeline {
position: relative;
padding-left: 2rem;
}
.timeline-item {
position: relative;
padding-bottom: 1.5rem;
}
.timeline-marker {
position: absolute;
left: -2rem;
top: 0.25rem;
width: 1rem;
height: 1rem;
border-radius: 50%;
}
.timeline-content {
margin-left: 1rem;
}
.timeline-title {
font-size: 1rem;
font-weight: 600;
margin-bottom: 0.25rem;
}
.timeline-description {
font-size: 0.875rem;
color: #6c757d;
margin-bottom: 0.25rem;
}
.alert {
border: none;
border-radius: 0.5rem;
}
.table {
margin-bottom: 0;
}
.table th {
border-top: none;
font-weight: 600;
color: #495057;
background-color: #f8f9fa;
}
.badge {
font-size: 0.75rem;
}
</style>

View File

@ -0,0 +1,456 @@
<template>
<div>
<!-- Header -->
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<h6 class="mb-0">Documents du praticien</h6>
<button @click="showAddModal = true" class="btn btn-sm btn-primary">
<i class="fas fa-plus me-1"></i>
Ajouter un document
</button>
</div>
</div>
</div>
<!-- Documents List -->
<div v-if="documents && documents.length > 0" class="row">
<div
v-for="document in documents"
:key="document.id"
class="col-md-6 col-lg-4 mb-4"
>
<div class="card card-stats card-round">
<div class="card-body">
<div class="row align-items-center">
<div class="col-icon">
<div
class="icon-big text-center icon-primary bubble-shadow-small"
>
<i class="fas fa-file-pdf"></i>
</div>
</div>
<div class="col col-stats ms-3 ms-sm-0 mt-3">
<div class="numbers">
<p class="card-category">
{{ getDocumentTypeLabel(document.type) }}
</p>
<h4 class="card-title">
{{ document.name || "Document sans nom" }}
</h4>
<small class="text-muted">{{
formatDate(document.created_at)
}}</small>
</div>
</div>
</div>
<div class="mt-3">
<div class="btn-group w-100" role="group">
<button
@click="downloadDocument(document)"
class="btn btn-outline-primary btn-sm"
>
<i class="fas fa-download me-1"></i>
Télécharger
</button>
<button
@click="editDocument(document)"
class="btn btn-outline-info btn-sm"
>
<i class="fas fa-edit me-1"></i>
Modifier
</button>
<button
@click="deleteDocument(document.id)"
class="btn btn-outline-danger btn-sm"
>
<i class="fas fa-trash me-1"></i>
Supprimer
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Empty State -->
<div v-else class="row">
<div class="col-12">
<div class="text-center p-5">
<div class="mb-3">
<i class="fas fa-file-alt fa-3x text-muted"></i>
</div>
<h5 class="text-muted">Aucun document</h5>
<p class="text-muted">
Commencez par ajouter des documents pour ce praticien.
</p>
<button @click="showAddModal = true" class="btn btn-primary">
<i class="fas fa-plus me-1"></i>
Ajouter le premier document
</button>
</div>
</div>
</div>
<!-- Add/Edit Document Modal -->
<div
v-if="showAddModal || showEditModal"
class="modal fade show"
style="display: block"
tabindex="-1"
role="dialog"
>
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
{{
showEditModal ? "Modifier le document" : "Ajouter un document"
}}
</h5>
<button
type="button"
class="btn-close"
@click="closeModal"
></button>
</div>
<div class="modal-body">
<form @submit.prevent="saveDocument" novalidate>
<div class="row">
<div class="col-12 mb-3">
<div class="form-group">
<label for="doc_name" class="form-label"
>Nom du document *</label
>
<soft-input
id="doc_name"
v-model="documentForm.name"
type="text"
:class="{ 'is-invalid': documentErrors.name }"
required
placeholder="Entrez le nom du document"
/>
<div v-if="documentErrors.name" class="invalid-feedback">
{{ documentErrors.name[0] }}
</div>
</div>
</div>
<div class="col-12 mb-3">
<div class="form-group">
<label for="doc_type" class="form-label"
>Type de document *</label
>
<select
id="doc_type"
v-model="documentForm.type"
class="form-control"
:class="{ 'is-invalid': documentErrors.type }"
required
>
<option value="">Sélectionnez un type</option>
<option value="license">Licence</option>
<option value="certificate">Certificat</option>
<option value="diploma">Diplôme</option>
<option value="authorization">Autorisation</option>
<option value="training">Formation</option>
<option value="other">Autre</option>
</select>
<div v-if="documentErrors.type" class="invalid-feedback">
{{ documentErrors.type[0] }}
</div>
</div>
</div>
<div class="col-12 mb-3">
<div class="form-group">
<label for="doc_file" class="form-label">Fichier</label>
<input
id="doc_file"
ref="fileInput"
type="file"
class="form-control"
:class="{ 'is-invalid': documentErrors.file }"
accept=".pdf,.doc,.docx,.jpg,.jpeg,.png"
@change="handleFileChange"
/>
<div v-if="documentErrors.file" class="invalid-feedback">
{{ documentErrors.file[0] }}
</div>
<small class="form-text text-muted">
Formats acceptés: PDF, DOC, DOCX, JPG, PNG (max 10MB)
</small>
</div>
</div>
<div
v-if="documentForm.type === 'authorization'"
class="col-12 mb-3"
>
<div class="form-group">
<label for="expiry_date" class="form-label"
>Date d'expiration</label
>
<soft-input
id="expiry_date"
v-model="documentForm.expiry_date"
type="date"
:class="{ 'is-invalid': documentErrors.expiry_date }"
/>
<div
v-if="documentErrors.expiry_date"
class="invalid-feedback"
>
{{ documentErrors.expiry_date[0] }}
</div>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-outline-secondary"
@click="closeModal"
>
Annuler
</button>
<button
type="button"
class="btn btn-primary"
@click="saveDocument"
:disabled="isDocumentLoading"
>
<span
v-if="isDocumentLoading"
class="spinner-border spinner-border-sm me-2"
role="status"
></span>
{{ showEditModal ? "Modifier" : "Ajouter" }}
</button>
</div>
</div>
</div>
</div>
<!-- Modal Backdrop -->
<div
v-if="showAddModal || showEditModal"
class="modal-backdrop fade show"
style="display: block"
@click="closeModal"
></div>
</div>
</template>
<script setup>
import { ref, reactive } from "vue";
import SoftInput from "@/components/SoftInput.vue";
import { defineProps, defineEmits } from "vue";
const props = defineProps({
documents: {
type: Array,
default: () => [],
},
employeeId: {
type: [Number, String],
required: true,
},
});
const emit = defineEmits([
"document-created",
"document-modified",
"document-removed",
]);
// Reactive data
const showAddModal = ref(false);
const showEditModal = ref(false);
const isDocumentLoading = ref(false);
const editingDocument = ref(null);
const fileInput = ref(null);
const documentErrors = reactive({});
// Form data
const documentForm = reactive({
name: "",
type: "",
file: null,
expiry_date: "",
});
const getDocumentTypeLabel = (type) => {
const labels = {
license: "Licence",
certificate: "Certificat",
diploma: "Diplôme",
authorization: "Autorisation",
training: "Formation",
other: "Autre",
};
return labels[type] || type;
};
const formatDate = (dateString) => {
if (!dateString) return "";
const date = new Date(dateString);
return date.toLocaleDateString("fr-FR");
};
const handleFileChange = (event) => {
const file = event.target.files[0];
documentForm.file = file;
};
const saveDocument = () => {
// Clear previous errors
Object.keys(documentErrors).forEach((key) => delete documentErrors[key]);
// Validate form
if (!documentForm.name.trim()) {
documentErrors.name = ["Le nom du document est obligatoire."];
return;
}
if (!documentForm.type) {
documentErrors.type = ["Le type de document est obligatoire."];
return;
}
isDocumentLoading.value = true;
try {
const documentData = {
...documentForm,
employee_id: props.employeeId,
};
if (showEditModal.value && editingDocument.value) {
documentData.id = editingDocument.value.id;
emit("document-modified", documentData);
} else {
emit("document-created", documentData);
}
closeModal();
} catch (error) {
console.error("Error saving document:", error);
documentErrors.general =
"Une erreur est survenue lors de l'enregistrement.";
} finally {
isDocumentLoading.value = false;
}
};
const editDocument = (document) => {
editingDocument.value = document;
documentForm.name = document.name || "";
documentForm.type = document.type || "";
documentForm.expiry_date = document.expiry_date || "";
documentForm.file = null; // Reset file
showEditModal.value = true;
};
const deleteDocument = (documentId) => {
if (confirm("Êtes-vous sûr de vouloir supprimer ce document ?")) {
emit("document-removed", documentId);
}
};
const downloadDocument = (document) => {
// TODO: Implement document download
console.log("Downloading document:", document);
// This would typically involve a file download API call
};
const closeModal = () => {
showAddModal.value = false;
showEditModal.value = false;
editingDocument.value = null;
// Reset form
documentForm.name = "";
documentForm.type = "";
documentForm.file = null;
documentForm.expiry_date = "";
// Clear errors
Object.keys(documentErrors).forEach((key) => delete documentErrors[key]);
// Reset file input
if (fileInput.value) {
fileInput.value.value = "";
}
};
</script>
<style scoped>
.card-stats {
border: none;
box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.075);
border-radius: 0.5rem;
}
.card-title {
font-size: 1rem;
margin-bottom: 0;
}
.card-category {
font-size: 0.875rem;
color: #6c757d;
margin-bottom: 0.25rem;
}
.form-label {
font-weight: 600;
color: #495057;
margin-bottom: 0.5rem;
}
.invalid-feedback {
display: block;
font-size: 0.875rem;
}
.modal {
background-color: rgba(0, 0, 0, 0.5);
}
.modal-content {
border: none;
border-radius: 0.5rem;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
}
.icon-big {
font-size: 2.5rem;
}
.bubble-shadow-small {
box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.1);
}
.col-icon {
flex: 0 0 auto;
width: 4.5rem;
}
.btn-group .btn {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
}
.spinner-border-sm {
width: 1rem;
height: 1rem;
}
.text-muted {
color: #6c757d;
}
</style>

View File

@ -0,0 +1,332 @@
<template>
<div>
<!-- Header -->
<div class="row mb-4">
<div class="col-12">
<h6 class="mb-0">Modifier les informations de l'employé</h6>
</div>
</div>
<!-- Form -->
<form @submit.prevent="handleSubmit" novalidate>
<div class="row">
<!-- Personal Information -->
<div class="col-12 mb-4">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Informations personnelles</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="first_name" class="form-label">Prénom *</label>
<soft-input
id="first_name"
v-model="formData.first_name"
type="text"
:class="{ 'is-invalid': errors.first_name }"
required
placeholder="Entrez le prénom"
/>
<div v-if="errors.first_name" class="invalid-feedback">
{{ errors.first_name[0] }}
</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="last_name" class="form-label">Nom *</label>
<soft-input
id="last_name"
v-model="formData.last_name"
type="text"
:class="{ 'is-invalid': errors.last_name }"
required
placeholder="Entrez le nom"
/>
<div v-if="errors.last_name" class="invalid-feedback">
{{ errors.last_name[0] }}
</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="email" class="form-label">Email</label>
<soft-input
id="email"
v-model="formData.email"
type="email"
:class="{ 'is-invalid': errors.email }"
placeholder="entreprise@exemple.com"
/>
<div v-if="errors.email" class="invalid-feedback">
{{ errors.email[0] }}
</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="phone" class="form-label">Téléphone</label>
<soft-input
id="phone"
v-model="formData.phone"
type="tel"
:class="{ 'is-invalid': errors.phone }"
placeholder="06 12 34 56 78"
/>
<div v-if="errors.phone" class="invalid-feedback">
{{ errors.phone[0] }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Employment Information -->
<div class="col-12 mb-4">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Informations d'emploi</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="hire_date" class="form-label"
>Date d'embauche *</label
>
<soft-input
id="hire_date"
v-model="formData.hire_date"
type="date"
:class="{ 'is-invalid': errors.hire_date }"
required
/>
<div v-if="errors.hire_date" class="invalid-feedback">
{{ errors.hire_date[0] }}
</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="job_title" class="form-label">Poste</label>
<soft-input
id="job_title"
v-model="formData.job_title"
type="text"
:class="{ 'is-invalid': errors.job_title }"
placeholder="Entrez le poste occupé"
/>
<div v-if="errors.job_title" class="invalid-feedback">
{{ errors.job_title[0] }}
</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="salary" class="form-label">Salaire ()</label>
<soft-input
id="salary"
v-model.number="formData.salary"
type="number"
step="0.01"
min="0"
:class="{ 'is-invalid': errors.salary }"
placeholder="0.00"
/>
<div v-if="errors.salary" class="invalid-feedback">
{{ errors.salary[0] }}
</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="active" class="form-label">Statut</label>
<select
id="active"
v-model="formData.active"
class="form-control"
:class="{ 'is-invalid': errors.active }"
>
<option value="1">Actif</option>
<option value="0">Inactif</option>
</select>
<div v-if="errors.active" class="invalid-feedback">
{{ errors.active[0] }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- General Error Message -->
<div v-if="errors.general" class="col-12">
<div class="alert alert-danger" role="alert">
<i class="fas fa-exclamation-triangle me-2"></i>
{{ errors.general }}
</div>
</div>
<!-- Form Actions -->
<div class="col-12">
<div class="d-flex justify-content-end">
<soft-button
type="button"
class="btn btn-light me-3"
@click="resetForm"
:disabled="isLoading"
>
Annuler
</soft-button>
<soft-button
type="submit"
class="btn bg-gradient-primary"
:disabled="isLoading"
>
<span
v-if="isLoading"
class="spinner-border spinner-border-sm me-2"
role="status"
></span>
<i v-else class="fas fa-save me-2"></i>
{{ isLoading ? "Enregistrement..." : "Enregistrer" }}
</soft-button>
</div>
</div>
</div>
</form>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from "vue";
import SoftInput from "@/components/SoftInput.vue";
import SoftButton from "@/components/SoftButton.vue";
import { defineProps, defineEmits } from "vue";
const props = defineProps({
employee: {
type: Object,
required: true,
},
});
const emit = defineEmits(["employee-updated"]);
// Reactive data
const isLoading = ref(false);
const errors = reactive({});
// Form data
const formData = reactive({
first_name: "",
last_name: "",
email: "",
phone: "",
hire_date: "",
job_title: "",
salary: 0,
active: true,
});
// Initialize form with employee data
onMounted(() => {
populateForm();
});
const populateForm = () => {
formData.first_name = props.employee.first_name || "";
formData.last_name = props.employee.last_name || "";
formData.email = props.employee.email || "";
formData.phone = props.employee.phone || "";
formData.hire_date = props.employee.hire_date || "";
formData.job_title = props.employee.job_title || "";
formData.salary = props.employee.salary || 0;
formData.active = props.employee.active ? 1 : 0;
// Clear errors
Object.keys(errors).forEach((key) => delete errors[key]);
};
const handleSubmit = async () => {
// Clear previous errors
Object.keys(errors).forEach((key) => delete errors[key]);
// Prepare data for submission
const submitData = {
first_name: formData.first_name.trim(),
last_name: formData.last_name.trim(),
email: formData.email.trim() || null,
phone: formData.phone.trim() || null,
hire_date: formData.hire_date,
job_title: formData.job_title.trim() || null,
salary: formData.salary || null,
active: formData.active === "1" || formData.active === 1,
};
try {
isLoading.value = true;
emit("employee-updated", submitData);
} catch (error) {
console.error("Error updating employee:", error);
if (error.response && error.response.status === 422) {
// Handle validation errors
Object.assign(errors, error.response.data.errors || {});
} else {
errors.general = "Une erreur est survenue lors de la mise à jour.";
}
} finally {
isLoading.value = false;
}
};
const resetForm = () => {
populateForm();
};
</script>
<style scoped>
.form-label {
font-weight: 600;
color: #495057;
margin-bottom: 0.5rem;
}
.invalid-feedback {
display: block;
font-size: 0.875rem;
}
.alert {
border: none;
border-radius: 0.5rem;
}
.card {
border: none;
box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.075);
border-radius: 0.5rem;
}
.card-title {
font-size: 1.1rem;
font-weight: 600;
color: #495057;
}
.spinner-border-sm {
width: 1rem;
height: 1rem;
}
</style>

View File

@ -0,0 +1,346 @@
<template>
<div>
<!-- Header -->
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<h6 class="mb-0">Aperçu de l'employé</h6>
<div>
<button
@click="$emit('view-info-tab')"
class="btn btn-sm btn-outline-primary"
>
<i class="fas fa-edit me-1"></i>
Modifier
</button>
</div>
</div>
</div>
</div>
<!-- Basic Information Cards -->
<div class="row">
<div class="col-md-6 mb-4">
<div class="card card-stats card-round">
<div class="card-body">
<div class="row align-items-center">
<div class="col-icon">
<div
class="icon-big text-center icon-warning bubble-shadow-small"
>
<i class="fas fa-user"></i>
</div>
</div>
<div class="col col-stats ms-3 ms-sm-0 mt-3">
<div class="numbers">
<p class="card-category">Prénom</p>
<h4 class="card-title">{{ employee.first_name }}</h4>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6 mb-4">
<div class="card card-stats card-round">
<div class="card-body">
<div class="row align-items-center">
<div class="col-icon">
<div
class="icon-big text-center icon-warning bubble-shadow-small"
>
<i class="fas fa-user"></i>
</div>
</div>
<div class="col col-stats ms-3 ms-sm-0 mt-3">
<div class="numbers">
<p class="card-category">Nom</p>
<h4 class="card-title">{{ employee.last_name }}</h4>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6 mb-4">
<div class="card card-stats card-round">
<div class="card-body">
<div class="row align-items-center">
<div class="col-icon">
<div
class="icon-big text-center icon-success bubble-shadow-small"
>
<i class="fas fa-envelope"></i>
</div>
</div>
<div class="col col-stats ms-3 ms-sm-0 mt-3">
<div class="numbers">
<p class="card-category">Email</p>
<h4 class="card-title">
{{ employee.email || "Non renseigné" }}
</h4>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6 mb-4">
<div class="card card-stats card-round">
<div class="card-body">
<div class="row align-items-center">
<div class="col-icon">
<div class="icon-big text-center icon-info bubble-shadow-small">
<i class="fas fa-phone"></i>
</div>
</div>
<div class="col col-stats ms-3 ms-sm-0 mt-3">
<div class="numbers">
<p class="card-category">Téléphone</p>
<h4 class="card-title">
{{ employee.phone || "Non renseigné" }}
</h4>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Employment Information -->
<div class="row">
<div class="col-12 mb-4">
<div class="card">
<div class="card-header">
<div class="card-head-row">
<div class="card-title">Informations d'emploi</div>
</div>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label class="form-label">Date d'embauche</label>
<p class="form-control-static">{{ formattedHireDate }}</p>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label class="form-label">Poste occupé</label>
<p class="form-control-static">
{{ employee.job_title || "Non renseigné" }}
</p>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label class="form-label">Salaire</label>
<p class="form-control-static">
{{
employee.salary ? `${employee.salary}` : "Non renseigné"
}}
</p>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label class="form-label">Statut</label>
<p class="form-control-static">
<span
:class="
employee.active
? 'badge bg-success'
: 'badge bg-secondary'
"
>
{{ employee.active ? "Actif" : "Inactif" }}
</span>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Specialization for Thanatopractitioners -->
<div
v-if="
employee.thanatopractitioner &&
employee.thanatopractitioner.license_number
"
class="row"
>
<div class="col-12 mb-4">
<div class="card">
<div class="card-header">
<div class="card-head-row">
<div class="card-title">Certification Thanatopractitioner</div>
</div>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label class="form-label">Numéro de licence</label>
<p class="form-control-static">
{{
employee.thanatopractitioner.license_number ||
"Non renseigné"
}}
</p>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label class="form-label">Numéro d'autorisation</label>
<p class="form-control-static">
{{
employee.thanatopractitioner.authorization_number ||
"Non renseigné"
}}
</p>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label class="form-label">Validité de l'autorisation</label>
<p class="form-control-static">
{{
formatDate(
employee.thanatopractitioner.authorization_valid_until
)
}}
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- System Information -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<div class="card-head-row">
<div class="card-title">Informations système</div>
</div>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label class="form-label">Date de création</label>
<p class="form-control-static">
{{ formatDate(employee.created_at) }}
</p>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label class="form-label">Dernière modification</label>
<p class="form-control-static">
{{ formatDate(employee.updated_at) }}
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { defineProps, defineEmits } from "vue";
defineProps({
employee: {
type: Object,
required: true,
},
formattedHireDate: {
type: String,
required: true,
},
employeeId: {
type: [Number, String],
required: true,
},
});
const emit = defineEmits(["view-info-tab"]);
const formatDate = (dateString) => {
if (!dateString) return "Non renseignée";
const date = new Date(dateString);
return date.toLocaleDateString("fr-FR", {
year: "numeric",
month: "long",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
});
};
</script>
<style scoped>
.card-stats {
border: none;
box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.075);
border-radius: 0.5rem;
}
.card-title {
font-size: 1.1rem;
margin-bottom: 0;
}
.card-category {
font-size: 0.875rem;
color: #6c757d;
margin-bottom: 0.25rem;
}
.form-label {
font-weight: 600;
color: #495057;
margin-bottom: 0.5rem;
}
.form-control-static {
padding: 0;
margin: 0;
border: none;
background-color: transparent;
color: #212529;
}
.icon-big {
font-size: 2.5rem;
}
.bubble-shadow-small {
box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.1);
}
.col-icon {
flex: 0 0 auto;
width: 4.5rem;
}
.card-head-row {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>

View File

@ -0,0 +1,420 @@
<template>
<div>
<!-- Header -->
<div class="row mb-4">
<div class="col-12">
<h6 class="mb-0">Informations Thanatopractitioner</h6>
</div>
</div>
<!-- Certification Information -->
<div class="row">
<div class="col-12 mb-4">
<div class="card">
<div class="card-header">
<div class="card-head-row">
<div class="card-title">Certification Professionnelle</div>
</div>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label class="form-label">Numéro de licence</label>
<p class="form-control-static">
{{ thanatopractitioner.license_number }}
</p>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label class="form-label">Numéro d'autorisation</label>
<p class="form-control-static">
{{ thanatopractitioner.authorization_number }}
</p>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label class="form-label">Date d'obtention</label>
<p class="form-control-static">
{{ formatDate(thanatopractitioner.created_at) }}
</p>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label class="form-label">Dernière mise à jour</label>
<p class="form-control-static">
{{ formatDate(thanatopractitioner.updated_at) }}
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Authorization Validity -->
<div class="row">
<div class="col-12 mb-4">
<div class="card">
<div class="card-header">
<div class="card-head-row">
<div class="card-title">Validité de l'autorisation</div>
</div>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-8">
<div class="form-group">
<label class="form-label">Date de fin de validité</label>
<p class="form-control-static">
{{
formatDate(thanatopractitioner.authorization_valid_until)
}}
</p>
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label class="form-label">Statut</label>
<p class="form-control-static">
<span
:class="
getAuthorizationStatusClass(
thanatopractitioner.authorization_valid_until
)
"
class="badge badge-sm"
>
{{
getAuthorizationStatusText(
thanatopractitioner.authorization_valid_until
)
}}
</span>
</p>
</div>
</div>
</div>
<!-- Validity Progress Bar -->
<div class="row mt-3">
<div class="col-12">
<div class="progress" style="height: 8px">
<div
:class="
getProgressBarClass(
thanatopractitioner.authorization_valid_until
)
"
class="progress-bar"
role="progressbar"
:style="`width: ${getValidityProgress(
thanatopractitioner.authorization_valid_until
)}%`"
:aria-valuenow="
getValidityProgress(
thanatopractitioner.authorization_valid_until
)
"
aria-valuemin="0"
aria-valuemax="100"
></div>
</div>
<small class="text-muted">
{{
getValidityText(
thanatopractitioner.authorization_valid_until
)
}}
</small>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Specialization Areas (Placeholder) -->
<div class="row">
<div class="col-12 mb-4">
<div class="card">
<div class="card-header">
<div class="card-head-row">
<div class="card-title">Spécialisations</div>
</div>
</div>
<div class="card-body">
<div class="row">
<div class="col-12">
<div class="alert alert-info" role="alert">
<i class="fas fa-info-circle me-2"></i>
<strong>À venir:</strong> Cette section affichera les
spécialisations du thanatopractitioner (ex: thanatopraxie,
thanatonomie, embaumement, etc.)
</div>
<div class="row mt-3">
<div class="col-md-4">
<div class="text-center p-3 border rounded">
<i class="fas fa-diagnoses fa-2x text-primary mb-2"></i>
<h6>Thanatopraxie</h6>
<small class="text-muted">Soins de conservation</small>
</div>
</div>
<div class="col-md-4">
<div class="text-center p-3 border rounded">
<i class="fas fa-microscope fa-2x text-success mb-2"></i>
<h6>Thanatonomie</h6>
<small class="text-muted">Anatomie et dissectie</small>
</div>
</div>
<div class="col-md-4">
<div class="text-center p-3 border rounded">
<i
class="fas fa-hand-holding-heart fa-2x text-warning mb-2"
></i>
<h6>Support aux familles</h6>
<small class="text-muted">Accompagnement</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Professional History (Placeholder) -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<div class="card-head-row">
<div class="card-title">Historique professionnel</div>
</div>
</div>
<div class="card-body">
<div class="row">
<div class="col-12">
<div class="alert alert-warning" role="alert">
<i class="fas fa-clock me-2"></i>
<strong>À venir:</strong> Cette section affichera l'historique
des interventions, formations et certifications du
thanatopractitioner.
</div>
<div class="timeline">
<div class="timeline-item">
<div class="timeline-marker bg-primary"></div>
<div class="timeline-content">
<h6 class="timeline-title">Obtention du certificat</h6>
<p class="timeline-description">
Certification initiale en thanatopraxie
</p>
<small class="text-muted">{{
formatDate(thanatopractitioner.created_at)
}}</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { defineProps } from "vue";
defineProps({
thanatopractitioner: {
type: Object,
required: true,
},
employee: {
type: Object,
required: true,
},
});
const formatDate = (dateString) => {
if (!dateString) return "Non renseignée";
const date = new Date(dateString);
return date.toLocaleDateString("fr-FR", {
year: "numeric",
month: "long",
day: "numeric",
});
};
const getAuthorizationStatusText = (validUntil) => {
if (!validUntil) return "Non définie";
const now = new Date();
const expiry = new Date(validUntil);
const diffTime = expiry.getTime() - now.getTime();
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
if (diffDays < 0) return "Expirée";
if (diffDays <= 30) return "Expire bientôt";
return "Valide";
};
const getAuthorizationStatusClass = (validUntil) => {
if (!validUntil) return "bg-secondary";
const now = new Date();
const expiry = new Date(validUntil);
const diffTime = expiry.getTime() - now.getTime();
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
if (diffDays < 0) return "bg-danger";
if (diffDays <= 30) return "bg-warning text-dark";
return "bg-success";
};
const getValidityProgress = (validUntil) => {
if (!validUntil) return 0;
const now = new Date();
const issued = new Date(); // Assume issued today for progress calculation
const expiry = new Date(validUntil);
// For demonstration, assume a 5-year validity period
const totalDays = 5 * 365;
const passedDays = Math.min(
totalDays,
Math.max(
0,
Math.floor((now.getTime() - issued.getTime()) / (1000 * 60 * 60 * 24))
)
);
return Math.round((passedDays / totalDays) * 100);
};
const getProgressBarClass = (validUntil) => {
if (!validUntil) return "bg-secondary";
const now = new Date();
const expiry = new Date(validUntil);
const diffTime = expiry.getTime() - now.getTime();
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
if (diffDays < 0) return "bg-danger";
if (diffDays <= 30) return "bg-warning";
return "bg-success";
};
const getValidityText = (validUntil) => {
if (!validUntil) return "Date d'expiration non définie";
const now = new Date();
const expiry = new Date(validUntil);
const diffTime = expiry.getTime() - now.getTime();
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
if (diffDays < 0) return `Expirée depuis ${Math.abs(diffDays)} jour(s)`;
if (diffDays === 0) return "Expire aujourd'hui";
if (diffDays === 1) return "Expire demain";
if (diffDays <= 30) return `Expire dans ${diffDays} jour(s)`;
return `Expire dans ${Math.floor(diffDays / 30)} mois`;
};
</script>
<style scoped>
.form-label {
font-weight: 600;
color: #495057;
margin-bottom: 0.5rem;
}
.form-control-static {
padding: 0;
margin: 0;
border: none;
background-color: transparent;
color: #212529;
}
.card {
border: none;
box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.075);
border-radius: 0.5rem;
}
.card-title {
font-size: 1.1rem;
font-weight: 600;
color: #495057;
}
.card-head-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.badge-sm {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
}
.progress {
border-radius: 0.5rem;
}
.alert {
border: none;
border-radius: 0.5rem;
}
.border {
border: 1px solid #dee2e6 !important;
}
.timeline {
position: relative;
padding-left: 2rem;
}
.timeline-item {
position: relative;
padding-bottom: 1.5rem;
}
.timeline-marker {
position: absolute;
left: -2rem;
top: 0.25rem;
width: 1rem;
height: 1rem;
border-radius: 50%;
}
.timeline-content {
margin-left: 1rem;
}
.timeline-title {
font-size: 1rem;
font-weight: 600;
margin-bottom: 0.25rem;
}
.timeline-description {
font-size: 0.875rem;
color: #6c757d;
margin-bottom: 0.25rem;
}
.rounded {
border-radius: 0.5rem;
}
</style>

View File

@ -0,0 +1,140 @@
<template>
<div class="card-body text-center">
<!-- Employee Avatar -->
<div class="avatar avatar-xl position-relative">
<img
v-if="avatarUrl"
:src="avatarUrl"
:alt="employeeName"
class="w-100 border-radius-lg shadow-sm"
/>
<div
v-else
class="avatar avatar-xl rounded-circle bg-gradient-primary text-white border-radius-lg shadow-sm d-flex align-items-center justify-content-center"
>
<span class="text-xl font-weight-bold">{{ initials }}</span>
</div>
<a
href="javascript:;"
class="btn btn-sm btn-icon-only bg-gradient-primary position-absolute bottom-0 end-0 mb-n2 me-n2"
@click="$emit('edit-avatar')"
>
<i class="fa fa-pen"></i>
</a>
</div>
<!-- Employee Name -->
<h5 class="font-weight-bolder mb-0">
{{ employeeName }}
</h5>
<p class="text-sm text-secondary mb-3">
{{ jobTitle }}
</p>
<!-- Quick Stats -->
<div class="row text-center mt-3">
<div class="col-6 border-end">
<h6 class="text-sm font-weight-bolder mb-0">
{{ hireDate }}
</h6>
<p class="text-xs text-secondary mb-0">Date embauche</p>
</div>
<div class="col-6">
<h6 class="text-sm font-weight-bolder mb-0">
<i
class="fas"
:class="
isActive
? 'fa-check-circle text-success'
: 'fa-times-circle text-danger'
"
></i>
</h6>
<p class="text-xs text-secondary mb-0">Statut</p>
</div>
</div>
<!-- Thanatopractitioner Badge -->
<div v-if="isThanatopractitioner" class="mt-3">
<span class="badge badge-sm bg-gradient-info">
<i class="fas fa-user-md me-1"></i>
Thanatopractitioner
</span>
</div>
</div>
</template>
<script setup>
import { defineProps, defineEmits } from "vue";
defineProps({
avatarUrl: {
type: String,
default: null,
},
initials: {
type: String,
required: true,
},
employeeName: {
type: String,
required: true,
},
jobTitle: {
type: String,
default: "Employé",
},
status: {
type: String,
default: "Actif",
},
hireDate: {
type: String,
required: true,
},
isActive: {
type: Boolean,
default: true,
},
isThanatopractitioner: {
type: Boolean,
default: false,
},
});
defineEmits(["edit-avatar"]);
</script>
<style scoped>
.avatar {
width: 4rem;
height: 4rem;
}
.avatar-xl {
width: 6rem;
height: 6rem;
}
.border-radius-lg {
border-radius: 0.5rem;
}
.bg-gradient-primary {
background: linear-gradient(310deg, #7928ca, #ff0080);
}
.bg-gradient-info {
background: linear-gradient(310deg, #0dcaf0, #6bb9f0);
}
.shadow-sm {
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
}
.btn-icon-only {
width: 2.5rem;
height: 2.5rem;
padding: 0.625rem;
}
</style>

View File

@ -0,0 +1,54 @@
<template>
<ul class="nav nav-pills flex-column">
<TabNavigationItem
icon="fas fa-eye"
label="Aperçu"
:is-active="activeTab === 'overview'"
spacing=""
@click="$emit('change-tab', 'overview')"
/>
<TabNavigationItem
icon="fas fa-info-circle"
label="Informations"
:is-active="activeTab === 'info'"
@click="$emit('change-tab', 'info')"
/>
<TabNavigationItem
icon="fas fa-file-alt"
label="Documents"
:is-active="activeTab === 'documents'"
@click="$emit('change-tab', 'documents')"
/>
<TabNavigationItem
v-if="isThanatopractitioner"
icon="fas fa-user-md"
label="Praticien"
:is-active="activeTab === 'practitioner'"
@click="$emit('change-tab', 'practitioner')"
/>
<TabNavigationItem
icon="fas fa-chart-line"
label="Activité"
:is-active="activeTab === 'activity'"
@click="$emit('change-tab', 'activity')"
/>
</ul>
</template>
<script setup>
import TabNavigationItem from "@/components/atoms/client/TabNavigationItem.vue";
import { defineProps, defineEmits } from "vue";
const props = defineProps({
activeTab: {
type: String,
required: true,
},
isThanatopractitioner: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(["change-tab"]);
</script>

View File

@ -0,0 +1,21 @@
<template>
<div class="container-fluid py-4">
<div class="row mb-4">
<slot name="button-return" />
</div>
<div class="row">
<div class="col-lg-3">
<slot name="employee-detail-sidebar" />
<slot name="file-input" />
</div>
<div class="col-lg-9 mt-lg-0 mt-4">
<slot name="loading-state" />
<slot name="employee-detail-content" />
</div>
</div>
</div>
</template>
<script setup>
// Template component - no logic needed
</script>

View File

@ -0,0 +1,24 @@
<template>
<div class="container-fluid py-4">
<div class="d-sm-flex justify-content-between">
<div>
<slot name="thanatopractitioner-new-action"></slot>
</div>
<div class="d-flex">
<div class="dropdown d-inline">
<slot name="select-filter"></slot>
</div>
<slot name="thanatopractitioner-other-action"></slot>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card mt-4">
<slot name="thanatopractitioner-table"></slot>
</div>
</div>
</div>
</div>
</template>
<script></script>

View File

@ -0,0 +1,271 @@
<template>
<div class="container-fluid py-4">
<!-- Example Usage in Employee Management -->
<div class="card">
<div class="card-body">
<h5 class="card-title">Confirmation Modal Examples</h5>
<!-- Delete Employee Example -->
<div class="mb-4">
<h6>Delete Employee Confirmation</h6>
<button class="btn btn-danger" @click="confirmDeleteEmployee">
<i class="fas fa-trash me-1"></i>
Supprimer l'employé
</button>
</div>
<!-- Other Confirmation Examples -->
<div class="mb-4">
<h6>Warning Example</h6>
<button class="btn btn-warning" @click="showWarning">
<i class="fas fa-exclamation-triangle me-1"></i>
Attention requise
</button>
</div>
<div class="mb-4">
<h6>Success Example</h6>
<button class="btn btn-success" @click="showSuccess">
<i class="fas fa-check me-1"></i>
Opération réussie
</button>
</div>
<div class="mb-4">
<h6>Info Example</h6>
<button class="btn btn-info" @click="showInfo">
<i class="fas fa-info me-1"></i>
Information
</button>
</div>
</div>
</div>
<!-- Reusable Confirm Modal -->
<confirm-modal
:is-visible="confirmModal.isVisible"
:title="confirmModal.title"
:message="confirmModal.message"
:details="confirmModal.details"
:type="confirmModal.type"
:confirm-text="confirmModal.confirmText"
:cancel-text="confirmModal.cancelText"
:is-loading="confirmModal.isLoading"
:show-cancel="confirmModal.showCancel"
:persistent="confirmModal.persistent"
@confirm="handleConfirm"
@cancel="handleCancel"
@close="handleCancel"
/>
</div>
</template>
<script setup>
import { reactive } from "vue";
import ConfirmModal from "@/components/molecules/common/ConfirmModal.vue";
import { useEmployeeStore } from "@/stores/employeeStore";
import { useNotificationStore } from "@/stores/notification";
import { useRouter } from "vue-router";
// Stores
const employeeStore = useEmployeeStore();
const notificationStore = useNotificationStore();
const router = useRouter();
// Reactive confirmation modal state
const confirmModal = reactive({
isVisible: false,
title: "",
message: "",
details: "",
type: "danger", // danger, warning, info, success
confirmText: "Confirmer",
cancelText: "Annuler",
isLoading: false,
showCancel: true,
persistent: false,
action: null,
data: null,
});
// Example employee data for demo
const employeeToDelete = {
id: 1,
first_name: "Jean",
last_name: "Dupont",
job_title: "Développeur",
};
// Show confirmation modal for employee deletion
const confirmDeleteEmployee = () => {
confirmModal.isVisible = true;
confirmModal.title = "Supprimer l'employé";
confirmModal.message = `Êtes-vous sûr de vouloir supprimer l'employé "${employeeToDelete.first_name} ${employeeToDelete.last_name}" ?`;
confirmModal.details =
"Cette action est irréversible. Toutes les données associées seront définitivement supprimées.";
confirmModal.type = "danger";
confirmModal.confirmText = "Supprimer";
confirmModal.cancelText = "Annuler";
confirmModal.showCancel = true;
confirmModal.persistent = false;
confirmModal.action = "deleteEmployee";
confirmModal.data = employeeToDelete;
};
// Show warning modal
const showWarning = () => {
confirmModal.isVisible = true;
confirmModal.title = "Attention";
confirmModal.message = "Cette action va modifier des données importantes.";
confirmModal.details = "Veuillez confirmer que vous souhaitez procéder.";
confirmModal.type = "warning";
confirmModal.confirmText = "Procéder";
confirmModal.cancelText = "Annuler";
confirmModal.showCancel = true;
confirmModal.persistent = false;
confirmModal.action = "warning";
confirmModal.data = null;
};
// Show success modal
const showSuccess = () => {
confirmModal.isVisible = true;
confirmModal.title = "Succès";
confirmModal.message = "L'opération a été réalisée avec succès !";
confirmModal.details = "Vos modifications ont été sauvegardées.";
confirmModal.type = "success";
confirmModal.confirmText = "Parfait";
confirmModal.cancelText = "";
confirmModal.showCancel = false;
confirmModal.persistent = true; // User must click confirm to close
confirmModal.action = "success";
confirmModal.data = null;
};
// Show info modal
const showInfo = () => {
confirmModal.isVisible = true;
confirmModal.title = "Information";
confirmModal.message =
"Voici des informations importantes concernant votre compte.";
confirmModal.details =
"Ces informations seront utiles pour la suite du processus.";
confirmModal.type = "info";
confirmModal.confirmText = "Compris";
confirmModal.cancelText = "";
confirmModal.showCancel = false;
confirmModal.persistent = true;
confirmModal.action = "info";
confirmModal.data = null;
};
// Handle confirm action
const handleConfirm = async () => {
confirmModal.isLoading = true;
try {
switch (confirmModal.action) {
case "deleteEmployee":
await deleteEmployee(confirmModal.data);
break;
case "warning":
notificationStore.info(
"Action procéder",
"Vous avez confirmé l'action."
);
break;
case "success":
notificationStore.success("Fermé", "Modal fermé.");
break;
case "info":
notificationStore.info(
"Information lue",
"Merci d'avoir lu l'information."
);
break;
}
// Close modal after successful action
closeModal();
} catch (error) {
console.error("Erreur lors de l'action:", error);
notificationStore.error(
"Erreur",
"Une erreur est survenue lors de l'opération."
);
} finally {
confirmModal.isLoading = false;
}
};
// Handle cancel action
const handleCancel = () => {
if (!confirmModal.isLoading) {
closeModal();
}
};
// Close modal
const closeModal = () => {
confirmModal.isVisible = false;
confirmModal.isLoading = false;
confirmModal.action = null;
confirmModal.data = null;
};
// Delete employee function
const deleteEmployee = async (employee) => {
try {
await employeeStore.deleteEmployee(employee.id);
notificationStore.success(
"Employé supprimé",
`L'employé ${employee.first_name} ${employee.last_name} a été supprimé avec succès.`
);
// Optional: Redirect to employee list or refresh data
// await employeeStore.fetchEmployees();
// router.push({ name: "Liste des employés" });
} catch (error) {
throw error; // Re-throw to be handled by handleConfirm
}
};
</script>
<style scoped>
.card {
border: none;
box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.075);
}
.btn {
border-radius: 0.375rem;
padding: 0.5rem 1rem;
font-weight: 500;
transition: all 0.2s ease;
}
.btn:hover {
transform: translateY(-1px);
}
.btn-danger {
background-color: #dc3545;
border-color: #dc3545;
}
.btn-warning {
background-color: #ffc107;
border-color: #ffc107;
color: #212529;
}
.btn-success {
background-color: #198754;
border-color: #198754;
}
.btn-info {
background-color: #0dcaf0;
border-color: #0dcaf0;
}
</style>

View File

@ -308,7 +308,7 @@ export default {
},
{
id: "employes-thanatopracteurs",
route: { name: "Employés Thanatopracteurs" },
route: { name: "Gestion thanatopractitioners" },
miniIcon: "T",
text: "Employés & Thanatopracteurs",
},

View File

@ -529,6 +529,11 @@ const routes = [
name: "Creation employé",
component: () => import("@/views/pages/CRM/AddEmployee.vue"),
},
{
path: "/employes/:id",
name: "Employee details",
component: () => import("@/views/pages/CRM/EmployeeDetails.vue"),
},
{
path: "/employes/ndf",
name: "NDF",
@ -546,10 +551,24 @@ const routes = [
},
{
path: "/employes/thanatopracteurs",
name: "Employés Thanatopracteurs",
name: "Gestion thanatopractitioners",
component: () =>
import("@/views/pages/Employes/EmployesThanatopracteurs.vue"),
import("@/views/pages/Thanatopractitioners/Thanatopractitioners.vue"),
},
{
path: "/employes/thanatopracteurs/new",
name: "Creation thanatopractitioner",
component: () =>
import("@/views/pages/Thanatopractitioners/AddThanatopractitioner.vue"),
},
// {
// path: "/employes/thanatopracteurs/:id",
// name: "Thanatopractitioner details",
// component: () =>
// import(
// "@/views/pages/Thanatopractitioners/ThanatopractitionerDetails.vue"
// ),
// },
// Paramétrage
{
path: "/parametrage/droits",

View File

@ -0,0 +1,286 @@
import { request } from "./http";
export interface Thanatopractitioner {
id: number;
employee_id: number;
diploma_number: string;
diploma_date: string;
authorization_number: string;
authorization_issue_date: string;
authorization_expiry_date: string;
notes: string | null;
active: boolean;
created_at: string;
updated_at: string;
// Relations
employee?: {
id: number;
first_name: string;
last_name: string;
full_name: string;
email: string | null;
phone: string | null;
job_title: string | null;
active: boolean;
} | null;
}
export interface ThanatopractitionerListResponse {
data: Thanatopractitioner[];
pagination: {
current_page: number;
per_page: number;
total: number;
last_page: number;
from: number;
to: number;
};
message: string;
}
// For nested response structure
export interface NestedThanatopractitionerListResponse {
data: {
data: Thanatopractitioner[];
pagination: {
current_page: number;
per_page: number;
total: number;
last_page: number;
from: number;
to: number;
};
message: string;
};
}
export interface ThanatopractitionerResponse {
data: Thanatopractitioner;
message: string;
}
export interface CreateThanatopractitionerPayload {
employee_id: number;
diploma_number: string;
diploma_date: string;
authorization_number: string;
authorization_issue_date: string;
authorization_expiry_date: string;
notes?: string | null;
active?: boolean;
}
export interface UpdateThanatopractitionerPayload
extends Partial<CreateThanatopractitionerPayload> {
id: number;
}
export const ThanatopractitionerService = {
/**
* Get all thanatopractitioners with pagination and filtering
*/
async getAllThanatopractitioners(params?: {
page?: number;
per_page?: number;
search?: string;
active?: boolean;
sort_by?: string;
sort_direction?: string;
}): Promise<NestedThanatopractitionerListResponse> {
const response = await request<NestedThanatopractitionerListResponse>({
url: "/api/thanatopractitioners",
method: "get",
params,
});
return response;
},
/**
* Get a specific thanatopractitioner by ID
*/
async getThanatopractitioner(
id: number
): Promise<ThanatopractitionerResponse> {
const response = await request<ThanatopractitionerResponse>({
url: `/api/thanatopractitioners/${id}`,
method: "get",
});
return response;
},
/**
* Create a new thanatopractitioner
*/
async createThanatopractitioner(
payload: CreateThanatopractitionerPayload
): Promise<ThanatopractitionerResponse> {
const formattedPayload = this.transformThanatopractitionerPayload(payload);
const response = await request<ThanatopractitionerResponse>({
url: "/api/thanatopractitioners",
method: "post",
data: formattedPayload,
});
return response;
},
/**
* Update an existing thanatopractitioner
*/
async updateThanatopractitioner(
payload: UpdateThanatopractitionerPayload
): Promise<ThanatopractitionerResponse> {
const { id, ...updateData } = payload;
const formattedPayload = this.transformThanatopractitionerPayload(
updateData
);
const response = await request<ThanatopractitionerResponse>({
url: `/api/thanatopractitioners/${id}`,
method: "put",
data: formattedPayload,
});
return response;
},
/**
* Delete a thanatopractitioner
*/
async deleteThanatopractitioner(
id: number
): Promise<{ success: boolean; message: string }> {
const response = await request<{ success: boolean; message: string }>({
url: `/api/thanatopractitioners/${id}`,
method: "delete",
});
return response;
},
/**
* Get active thanatopractitioners only
*/
async getActiveThanatopractitioners(params?: {
page?: number;
per_page?: number;
}): Promise<NestedThanatopractitionerListResponse> {
const response = await request<NestedThanatopractitionerListResponse>({
url: "/api/thanatopractitioners",
method: "get",
params: {
active: true,
...params,
},
});
return response;
},
/**
* Get thanatopractitioner statistics
*/
async getStatistics(): Promise<{
data: {
total_thanatopractitioners: number;
active_thanatopractitioners: number;
inactive_thanatopractitioners: number;
authorizations_expiring_soon: number;
};
}> {
const response = await request<{
data: {
total_thanatopractitioners: number;
active_thanatopractitioners: number;
inactive_thanatopractitioners: number;
authorizations_expiring_soon: number;
};
}>({
url: "/api/thanatopractitioners/statistics",
method: "get",
});
return response;
},
/**
* Transform thanatopractitioner payload to match Laravel form request structure
*/
transformThanatopractitionerPayload(
payload: Partial<CreateThanatopractitionerPayload>
): any {
const transformed: any = { ...payload };
// Ensure boolean values are properly formatted
if (typeof transformed.active === "boolean") {
transformed.active = transformed.active ? 1 : 0;
}
// Validate and format dates
const dateFields = [
"diploma_date",
"authorization_issue_date",
"authorization_expiry_date",
];
dateFields.forEach((field) => {
if (transformed[field]) {
const date = new Date(transformed[field]);
if (!isNaN(date.getTime())) {
transformed[field] = date.toISOString().split("T")[0];
}
}
});
// Remove undefined values to avoid sending them
Object.keys(transformed).forEach((key) => {
if (transformed[key] === undefined) {
delete transformed[key];
}
});
return transformed;
},
/**
* Search thanatopractitioners by name, email, or other criteria
*/
async searchThanatopractitioners(
query: string
): Promise<Thanatopractitioner[]> {
const response = await request<{
data: {
data: Thanatopractitioner[];
};
}>({
url: "/api/thanatopractitioners",
method: "get",
params: {
search: query,
},
});
return response.data.data;
},
/**
* Toggle thanatopractitioner active status
*/
async toggleThanatopractitionerStatus(
id: number,
isActive: boolean
): Promise<ThanatopractitionerResponse> {
const response = await request<ThanatopractitionerResponse>({
url: `/api/thanatopractitioners/${id}`,
method: "put",
data: {
active: isActive,
},
});
return response;
},
};
export default ThanatopractitionerService;

View File

@ -79,6 +79,16 @@ export const useEmployeeStore = defineStore("employee", () => {
from: meta.from || 0,
to: meta.to || 0,
};
} else {
// Reset pagination if no meta provided
pagination.value = {
current_page: 1,
last_page: 1,
per_page: 10,
total: 0,
from: 0,
to: 0,
};
}
};

View File

@ -0,0 +1,421 @@
import { defineStore } from "pinia";
import { ref, computed } from "vue";
import ThanatopractitionerService from "@/services/thanatopractitioner";
import type {
Thanatopractitioner,
CreateThanatopractitionerPayload,
UpdateThanatopractitionerPayload,
ThanatopractitionerListResponse,
NestedThanatopractitionerListResponse,
} from "@/services/thanatopractitioner";
export const useThanatopractitionerStore = defineStore(
"thanatopractitioner",
() => {
// State
const thanatopractitioners = ref<Thanatopractitioner[]>([]);
const currentThanatopractitioner = ref<Thanatopractitioner | null>(null);
const loading = ref(false);
const error = ref<string | null>(null);
const searchResults = ref<Thanatopractitioner[]>([]);
// Pagination state
const pagination = ref({
current_page: 1,
last_page: 1,
per_page: 10,
total: 0,
from: 0,
to: 0,
});
// Getters
const allThanatopractitioners = computed(() => thanatopractitioners.value);
const activeThanatopractitioners = computed(() =>
thanatopractitioners.value.filter(
(thanatopractitioner: Thanatopractitioner) => thanatopractitioner.active
)
);
const inactiveThanatopractitioners = computed(() =>
thanatopractitioners.value.filter(
(thanatopractitioner: Thanatopractitioner) =>
!thanatopractitioner.active
)
);
const isLoading = computed(() => loading.value);
const hasError = computed(() => error.value !== null);
const getError = computed(() => error.value);
const getThanatopractitionerById = computed(() => (id: number) =>
thanatopractitioners.value.find(
(thanatopractitioner: Thanatopractitioner) =>
thanatopractitioner.id === id
)
);
const getPagination = computed(() => pagination.value);
// Actions
const setLoading = (isLoading: boolean) => {
loading.value = isLoading;
};
const setError = (err: string | null) => {
error.value = err;
};
const clearError = () => {
error.value = null;
};
const setThanatopractitioners = (
newThanatopractitioners: Thanatopractitioner[]
) => {
thanatopractitioners.value = newThanatopractitioners;
};
const setCurrentThanatopractitioner = (
thanatopractitioner: Thanatopractitioner | null
) => {
currentThanatopractitioner.value = thanatopractitioner;
};
const setSearchThanatopractitioner = (
searchThanatopractitioner: Thanatopractitioner[]
) => {
searchResults.value = searchThanatopractitioner;
};
const setPagination = (meta: any) => {
if (meta) {
pagination.value = {
current_page: meta.current_page || 1,
last_page: meta.last_page || 1,
per_page: meta.per_page || 10,
total: meta.total || 0,
from: meta.from || 0,
to: meta.to || 0,
};
}
};
/**
* Fetch all thanatopractitioners with optional pagination and filters
*/
const fetchThanatopractitioners = async (params?: {
page?: number;
per_page?: number;
search?: string;
active?: boolean;
sort_by?: string;
sort_direction?: string;
}) => {
setLoading(true);
setError(null);
try {
const response = await ThanatopractitionerService.getAllThanatopractitioners(
params
);
setThanatopractitioners(response.data.data);
setPagination(response.data.pagination);
return response;
} catch (err: any) {
const errorMessage =
err.response?.data?.message ||
err.message ||
"Failed to fetch thanatopractitioners";
setError(errorMessage);
throw err;
} finally {
setLoading(false);
}
};
/**
* Fetch a single thanatopractitioner by ID
*/
const fetchThanatopractitioner = async (id: number) => {
setLoading(true);
setError(null);
try {
const response = await ThanatopractitionerService.getThanatopractitioner(
id
);
setCurrentThanatopractitioner(response.data);
return response.data;
} catch (err: any) {
const errorMessage =
err.response?.data?.message ||
err.message ||
"Failed to fetch thanatopractitioner";
setError(errorMessage);
throw err;
} finally {
setLoading(false);
}
};
/**
* Create a new thanatopractitioner
*/
const createThanatopractitioner = async (
payload: CreateThanatopractitionerPayload
) => {
setLoading(true);
setError(null);
try {
const response = await ThanatopractitionerService.createThanatopractitioner(
payload
);
// Add the new thanatopractitioner to the list
thanatopractitioners.value.push(response.data);
setCurrentThanatopractitioner(response.data);
return response.data;
} catch (err: any) {
const errorMessage =
err.response?.data?.message ||
err.message ||
"Failed to create thanatopractitioner";
setError(errorMessage);
throw err;
} finally {
setLoading(false);
}
};
/**
* Update an existing thanatopractitioner
*/
const updateThanatopractitioner = async (
payload: UpdateThanatopractitionerPayload
) => {
setLoading(true);
setError(null);
try {
console.log(payload);
const response = await ThanatopractitionerService.updateThanatopractitioner(
payload
);
const updatedThanatopractitioner = response.data;
// Update in the thanatopractitioners list
const index = thanatopractitioners.value.findIndex(
(thanatopractitioner: Thanatopractitioner) =>
thanatopractitioner.id === updatedThanatopractitioner.id
);
if (index !== -1) {
thanatopractitioners.value[index] = updatedThanatopractitioner;
}
// Update current thanatopractitioner if it's the one being edited
if (
currentThanatopractitioner.value &&
currentThanatopractitioner.value.id === updatedThanatopractitioner.id
) {
setCurrentThanatopractitioner(updatedThanatopractitioner);
}
return updatedThanatopractitioner;
} catch (err: any) {
const errorMessage =
err.response?.data?.message ||
err.message ||
"Failed to update thanatopractitioner";
setError(errorMessage);
throw err;
} finally {
setLoading(false);
}
};
/**
* Delete a thanatopractitioner
*/
const deleteThanatopractitioner = async (id: number) => {
setLoading(true);
setError(null);
try {
const response = await ThanatopractitionerService.deleteThanatopractitioner(
id
);
// Remove from the thanatopractitioners list
thanatopractitioners.value = thanatopractitioners.value.filter(
(thanatopractitioner: Thanatopractitioner) =>
thanatopractitioner.id !== id
);
// Clear current thanatopractitioner if it's the one being deleted
if (
currentThanatopractitioner.value &&
currentThanatopractitioner.value.id === id
) {
setCurrentThanatopractitioner(null);
}
return response;
} catch (err: any) {
const errorMessage =
err.response?.data?.message ||
err.message ||
"Failed to delete thanatopractitioner";
setError(errorMessage);
throw err;
} finally {
setLoading(false);
}
};
/**
* Search thanatopractitioners
*/
const searchThanatopractitioners = async (query: string) => {
setLoading(true);
error.value = null;
try {
const results = await ThanatopractitionerService.searchThanatopractitioners(
query
);
setSearchThanatopractitioner(results);
return results;
} catch (err) {
error.value = "Erreur lors de la recherche des thanatopractitioners";
console.error("Error searching thanatopractitioners:", err);
setSearchThanatopractitioner([]);
throw err;
} finally {
setLoading(false);
}
};
/**
* Toggle thanatopractitioner active status
*/
const toggleThanatopractitionerStatus = async (
id: number,
isActive: boolean
) => {
setLoading(true);
setError(null);
try {
const response = await ThanatopractitionerService.toggleThanatopractitionerStatus(
id,
isActive
);
const updatedThanatopractitioner = response.data;
// Update in the thanatopractitioners list
const index = thanatopractitioners.value.findIndex(
(thanatopractitioner: Thanatopractitioner) =>
thanatopractitioner.id === id
);
if (index !== -1) {
thanatopractitioners.value[index] = updatedThanatopractitioner;
}
// Update current thanatopractitioner if it's the one being toggled
if (
currentThanatopractitioner.value &&
currentThanatopractitioner.value.id === id
) {
setCurrentThanatopractitioner(updatedThanatopractitioner);
}
return updatedThanatopractitioner;
} catch (err: any) {
const errorMessage =
err.response?.data?.message ||
err.message ||
"Failed to toggle thanatopractitioner status";
setError(errorMessage);
throw err;
} finally {
setLoading(false);
}
};
/**
* Get thanatopractitioner statistics
*/
const fetchStatistics = async () => {
setLoading(true);
setError(null);
try {
const response = await ThanatopractitionerService.getStatistics();
return response.data;
} catch (err: any) {
const errorMessage =
err.response?.data?.message ||
err.message ||
"Failed to fetch statistics";
setError(errorMessage);
throw err;
} finally {
setLoading(false);
}
};
/**
* Clear current thanatopractitioner
*/
const clearCurrentThanatopractitioner = () => {
setCurrentThanatopractitioner(null);
};
/**
* Clear all state
*/
const clearStore = () => {
thanatopractitioners.value = [];
currentThanatopractitioner.value = null;
error.value = null;
pagination.value = {
current_page: 1,
last_page: 1,
per_page: 10,
total: 0,
from: 0,
to: 0,
};
};
return {
// State
thanatopractitioners,
currentThanatopractitioner,
loading,
error,
searchResults,
// Getters
allThanatopractitioners,
activeThanatopractitioners,
inactiveThanatopractitioners,
isLoading,
hasError,
getError,
getThanatopractitionerById,
getPagination,
// Actions
fetchThanatopractitioners,
fetchThanatopractitioner,
createThanatopractitioner,
updateThanatopractitioner,
deleteThanatopractitioner,
searchThanatopractitioners,
toggleThanatopractitionerStatus,
fetchStatistics,
clearCurrentThanatopractitioner,
clearStore,
clearError,
};
}
);

View File

@ -0,0 +1,107 @@
<template>
<employee-detail-presentation
v-if="employeeStore.currentEmployee"
:employee="employeeStore.currentEmployee"
:is-loading="employeeStore.isLoading"
:employee-avatar="employeeAvatar"
:active-tab="activeTab"
:file-input="fileInput"
:thanatopractitioner-data="thanatopractitionerData"
:practitioner-documents="practitionerDocuments"
@update-the-employee="updateEmployee"
@create-practitioner-document="createPractitionerDocument"
@updating-practitioner-document="updatePractitionerDocument"
@remove-practitioner-document="removePractitionerDocument"
/>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { useRoute } from "vue-router";
import { useEmployeeStore } from "@/stores/employeeStore";
import { useNotificationStore } from "@/stores/notification";
import EmployeeDetailPresentation from "@/components/Organism/CRM/EmployeeDetailPresentation.vue";
const route = useRoute();
const employeeStore = useEmployeeStore();
const notificationStore = useNotificationStore();
// Ensure employee_id is a number
const employee_id = Number(route.params.id);
const activeTab = ref("overview");
const employeeAvatar = ref(null);
const fileInput = ref(null);
const thanatopractitionerData = ref(null);
const practitionerDocuments = ref([]);
onMounted(async () => {
if (employee_id) {
await employeeStore.fetchEmployee(employee_id);
// Check if employee has thanatopractitioner data
if (employeeStore.currentEmployee?.thanatopractitioner) {
thanatopractitionerData.value =
employeeStore.currentEmployee.thanatopractitioner;
// TODO: Fetch practitioner documents when API is available
// practitionerDocuments.value = await practitionerDocumentStore.getEmployeePractitionerDocuments(employee_id);
}
}
});
const updateEmployee = async (data) => {
if (!employee_id) {
console.error("Missing employee id");
notificationStore.error("Erreur", "ID de l'employé manquant");
return;
}
try {
// If data is FormData (e.g. file upload), append the id instead of spreading
if (data instanceof FormData) {
data.set("id", String(employee_id));
await employeeStore.updateEmployee(data);
} else {
await employeeStore.updateEmployee({ id: employee_id, ...data });
}
notificationStore.updated("Employé");
} catch (error) {
console.error("Error updating employee:", error);
notificationStore.error("Erreur", "Impossible de mettre à jour l'employé");
}
};
const createPractitionerDocument = async (data) => {
try {
// TODO: Implement when practitioner document store is available
// await practitionerDocumentStore.createPractitionerDocument(data);
// practitionerDocuments.value = await practitionerDocumentStore.getEmployeePractitionerDocuments(employee_id);
notificationStore.created("Document");
} catch (error) {
console.error("Error creating practitioner document:", error);
notificationStore.error("Erreur", "Impossible de créer le document");
}
};
const updatePractitionerDocument = async (modifiedDocument) => {
try {
// TODO: Implement when practitioner document store is available
// await practitionerDocumentStore.updatePractitionerDocument(modifiedDocument);
notificationStore.updated("Document");
} catch (error) {
console.error("Error updating practitioner document:", error);
notificationStore.error("Erreur", "Impossible de modifier le document");
}
};
const removePractitionerDocument = async (documentId) => {
try {
// TODO: Implement when practitioner document store is available
// await practitionerDocumentStore.deletePractitionerDocument(documentId);
notificationStore.deleted("Document");
} catch (error) {
console.error("Error removing practitioner document:", error);
notificationStore.error("Erreur", "Impossible de supprimer le document");
}
};
</script>

View File

@ -2,21 +2,55 @@
<employee-presentation
:employee-data="employeeStore.employees"
:loading-data="employeeStore.loading"
:pagination="employeeStore.pagination"
:pagination="employeeStore.getPagination"
@push-details="goDetails"
@delete-employee="deleteEmployee"
@delete-employee="confirmDeleteEmployee"
@change-page="changePage"
/>
<!-- Confirm Delete Modal -->
<confirm-modal
:is-visible="confirmModal.isVisible"
:title="confirmModal.title"
:message="confirmModal.message"
:details="confirmModal.details"
:type="confirmModal.type"
:confirm-text="confirmModal.confirmText"
:cancel-text="confirmModal.cancelText"
:is-loading="confirmModal.isLoading"
@confirm="handleConfirmDelete"
@cancel="handleCancelDelete"
@close="handleCancelDelete"
/>
</template>
<script setup>
import { reactive, onMounted } from "vue";
import EmployeePresentation from "@/components/Organism/Employee/EmployeePresentation.vue";
import ConfirmModal from "@/components/molecules/common/ConfirmModal.vue";
import { useEmployeeStore } from "@/stores/employeeStore";
import { onMounted } from "vue";
import { useNotificationStore } from "@/stores/notification";
import { useRouter } from "vue-router";
const employeeStore = useEmployeeStore();
const notificationStore = useNotificationStore();
const router = useRouter();
// Confirm modal state
const confirmModal = reactive({
isVisible: false,
title: "Supprimer l'employé",
message: "",
details:
"Cette action est irréversible. Toutes les données associées seront définitivement supprimées.",
type: "danger",
confirmText: "Supprimer",
cancelText: "Annuler",
isLoading: false,
employeeId: null,
employeeName: "",
});
onMounted(async () => {
await employeeStore.fetchEmployees();
});
@ -30,24 +64,95 @@ const goDetails = (id) => {
});
};
const deleteEmployee = async (employeeId) => {
try {
if (confirm("Êtes-vous sûr de vouloir supprimer cet employé ?")) {
await employeeStore.deleteEmployee(employeeId);
alert("Employé supprimé avec succès");
const confirmDeleteEmployee = (employeeId) => {
console.log("confirmDeleteEmployee called with ID:", employeeId);
console.log("Employee ID type:", typeof employeeId);
console.log("Store employees:", employeeStore.employees);
console.log("Store employees length:", employeeStore.employees.length);
const employee = employeeStore.employees.find((emp) => emp.id === employeeId);
console.log("Found employee:", employee);
if (employee) {
console.log("Showing confirmation modal");
confirmModal.isVisible = true;
confirmModal.employeeId = employeeId;
confirmModal.employeeName = `${employee.first_name} ${employee.last_name}`;
confirmModal.message = `Êtes-vous sûr de vouloir supprimer l'employé "${confirmModal.employeeName}" ?`;
} else {
console.log("No employee found - trying to parse as number");
const numericId = parseInt(employeeId, 10);
console.log("Trying with numeric ID:", numericId);
const employeeByNumber = employeeStore.employees.find(
(emp) => emp.id === numericId
);
console.log("Found by numeric ID:", employeeByNumber);
if (employeeByNumber) {
console.log("Showing confirmation modal with numeric ID");
confirmModal.isVisible = true;
confirmModal.employeeId = numericId;
confirmModal.employeeName = `${employeeByNumber.first_name} ${employeeByNumber.last_name}`;
confirmModal.message = `Êtes-vous sûr de vouloir supprimer l'employé "${confirmModal.employeeName}" ?`;
} else {
console.log(
"Still no employee found - displaying all employee IDs for comparison"
);
employeeStore.employees.forEach((emp, index) => {
console.log(
`Employee ${index}: ID=${emp.id} (${typeof emp.id}), Name=${
emp.first_name
} ${emp.last_name}`
);
});
}
} catch (error) {
console.error("Error deleting employee:", error);
alert("Erreur lors de la suppression de l'employé");
}
};
const changePage = async (page) => {
const handleConfirmDelete = async () => {
const employeeId = confirmModal.employeeId;
const employeeName = confirmModal.employeeName;
console.log("Test");
try {
await employeeStore.fetchEmployees({ page });
confirmModal.isLoading = true;
await employeeStore.deleteEmployee(employeeId);
notificationStore.success(
"Employé supprimé",
`L'employé ${employeeName} a été supprimé avec succès.`
);
closeConfirmModal();
} catch (error) {
console.error("Error deleting employee:", error);
notificationStore.error(
"Erreur de suppression",
"Une erreur est survenue lors de la suppression de l'employé."
);
closeConfirmModal();
}
};
const handleCancelDelete = () => {
closeConfirmModal();
};
const closeConfirmModal = () => {
confirmModal.isVisible = false;
confirmModal.employeeId = null;
confirmModal.employeeName = "";
confirmModal.isLoading = false;
};
const changePage = async (page) => {
console.log("changePage called in Employees.vue with page:", page);
try {
console.log("Fetching employees with page:", page);
await employeeStore.fetchEmployees({ page, per_page: 10 });
} catch (error) {
console.error("Error changing page:", error);
alert("Erreur lors du changement de page");
notificationStore.error(
"Erreur de pagination",
"Une erreur est survenue lors du changement de page."
);
}
};
</script>

View File

@ -0,0 +1,70 @@
<template>
<add-thanatopractitioner-presentation
:employees="employeeStore.employees"
:loading="thanatopractitionerStore.isLoading"
:validation-errors="validationErrors"
:success="showSuccess"
@create-thanatopractitioner="handleCreateThanatopractitioner"
/>
</template>
<script setup>
import AddThanatopractitionerPresentation from "@/components/Organism/Thanatopractitioner/AddThanatopractitionerPresentation.vue";
import { useThanatopractitionerStore } from "@/stores/thanatopractitionerStore";
import { useEmployeeStore } from "@/stores/employeeStore";
import { useNotificationStore } from "@/stores/notification";
import { ref, onMounted } from "vue";
import { useRouter } from "vue-router";
const router = useRouter();
const thanatopractitionerStore = useThanatopractitionerStore();
const employeeStore = useEmployeeStore();
const notificationStore = useNotificationStore();
const validationErrors = ref({});
const showSuccess = ref(false);
onMounted(async () => {
// Fetch active employees for selection
await employeeStore.fetchEmployees();
});
const handleCreateThanatopractitioner = async (form) => {
try {
// Clear previous errors
validationErrors.value = {};
showSuccess.value = false;
// Call the store to create thanatopractitioner
const thanatopractitioner = await thanatopractitionerStore.createThanatopractitioner(
form
);
// Show success notification
notificationStore.created("Thanatopractitioner");
showSuccess.value = true;
// Redirect after 2 seconds
setTimeout(() => {
router.push({ name: "Gestion thanatopractitioners" });
}, 2000);
} catch (error) {
console.error("Error creating thanatopractitioner:", error);
// Handle validation errors from Laravel
if (error.response && error.response.status === 422) {
validationErrors.value = error.response.data.errors || {};
notificationStore.error(
"Erreur de validation",
"Veuillez corriger les erreurs dans le formulaire"
);
} else if (error.response && error.response.data) {
// Handle other API errors
const errorMessage =
error.response.data.message || "Une erreur est survenue";
notificationStore.error("Erreur", errorMessage);
} else {
notificationStore.error("Erreur", "Une erreur inattendue s'est produite");
}
}
};
</script>

View File

@ -0,0 +1,174 @@
<template>
<thanatopractitioner-presentation
:thanatopractitioner-data="thanatopractitionerStore.thanatopractitioners"
:loading-data="thanatopractitionerStore.loading"
:pagination="thanatopractitionerStore.getPagination"
@push-details="goDetails"
@delete-thanatopractitioner="confirmDeleteThanatopractitioner"
@change-page="changePage"
/>
<!-- Confirm Delete Modal -->
<confirm-modal
:is-visible="confirmModal.isVisible"
:title="confirmModal.title"
:message="confirmModal.message"
:details="confirmModal.details"
:type="confirmModal.type"
:confirm-text="confirmModal.confirmText"
:cancel-text="confirmModal.cancelText"
:is-loading="confirmModal.isLoading"
@confirm="handleConfirmDelete"
@cancel="handleCancelDelete"
@close="handleCancelDelete"
/>
</template>
<script setup>
import { reactive, onMounted } from "vue";
import ThanatopractitionerPresentation from "@/components/Organism/Thanatopractitioner/ThanatopractitionerPresentation.vue";
import ConfirmModal from "@/components/molecules/common/ConfirmModal.vue";
import { useThanatopractitionerStore } from "@/stores/thanatopractitionerStore";
import { useNotificationStore } from "@/stores/notification";
import { useRouter } from "vue-router";
const thanatopractitionerStore = useThanatopractitionerStore();
const notificationStore = useNotificationStore();
const router = useRouter();
// Confirm modal state
const confirmModal = reactive({
isVisible: false,
title: "Supprimer le thanatopractitioner",
message: "",
details:
"Cette action est irréversible. Toutes les données associées seront définitivement supprimées.",
type: "danger",
confirmText: "Supprimer",
cancelText: "Annuler",
isLoading: false,
thanatopractitionerId: null,
thanatopractitionerName: "",
});
onMounted(async () => {
await thanatopractitionerStore.fetchThanatopractitioners();
});
const goDetails = (id) => {
router.push({
name: "Thanatopractitioner details",
params: {
id: id,
},
});
};
const confirmDeleteThanatopractitioner = (thanatopractitionerId) => {
console.log(
"confirmDeleteThanatopractitioner called with ID:",
thanatopractitionerId
);
console.log("Thanatopractitioner ID type:", typeof thanatopractitionerId);
console.log(
"Store thanatopractitioners:",
thanatopractitionerStore.thanatopractitioners
);
console.log(
"Store thanatopractitioners length:",
thanatopractitionerStore.thanatopractitioners.length
);
const thanatopractitioner = thanatopractitionerStore.thanatopractitioners.find(
(prac) => prac.id === thanatopractitionerId
);
console.log("Found thanatopractitioner:", thanatopractitioner);
if (thanatopractitioner) {
console.log("Showing confirmation modal");
confirmModal.isVisible = true;
confirmModal.thanatopractitionerId = thanatopractitionerId;
confirmModal.thanatopractitionerName = `${thanatopractitioner.first_name} ${thanatopractitioner.last_name}`;
confirmModal.message = `Êtes-vous sûr de vouloir supprimer le thanatopractitioner "${confirmModal.thanatopractitionerName}" ?`;
} else {
console.log("No thanatopractitioner found - trying to parse as number");
const numericId = parseInt(thanatopractitionerId, 10);
console.log("Trying with numeric ID:", numericId);
const thanatopractitionerByNumber = thanatopractitionerStore.thanatopractitioners.find(
(prac) => prac.id === numericId
);
console.log("Found by numeric ID:", thanatopractitionerByNumber);
if (thanatopractitionerByNumber) {
console.log("Showing confirmation modal with numeric ID");
confirmModal.isVisible = true;
confirmModal.thanatopractitionerId = numericId;
confirmModal.thanatopractitionerName = `${thanatopractitionerByNumber.first_name} ${thanatopractitionerByNumber.last_name}`;
confirmModal.message = `Êtes-vous sûr de vouloir supprimer le thanatopractitioner "${confirmModal.thanatopractitionerName}" ?`;
} else {
console.log(
"Still no thanatopractitioner found - displaying all thanatopractitioner IDs for comparison"
);
thanatopractitionerStore.thanatopractitioners.forEach((prac, index) => {
console.log(
`Thanatopractitioner ${index}: ID=${
prac.id
} (${typeof prac.id}), Name=${prac.first_name} ${prac.last_name}`
);
});
}
}
};
const handleConfirmDelete = async () => {
const thanatopractitionerId = confirmModal.thanatopractitionerId;
const thanatopractitionerName = confirmModal.thanatopractitionerName;
console.log("Test");
try {
confirmModal.isLoading = true;
await thanatopractitionerStore.deleteThanatopractitioner(
thanatopractitionerId
);
notificationStore.success(
"Thanatopractitioner supprimé",
`Le thanatopractitioner ${thanatopractitionerName} a été supprimé avec succès.`
);
closeConfirmModal();
} catch (error) {
console.error("Error deleting thanatopractitioner:", error);
notificationStore.error(
"Erreur de suppression",
"Une erreur est survenue lors de la suppression du thanatopractitioner."
);
closeConfirmModal();
}
};
const handleCancelDelete = () => {
closeConfirmModal();
};
const closeConfirmModal = () => {
confirmModal.isVisible = false;
confirmModal.thanatopractitionerId = null;
confirmModal.thanatopractitionerName = "";
confirmModal.isLoading = false;
};
const changePage = async (page) => {
console.log("changePage called in Thanatopractitioners.vue with page:", page);
try {
console.log("Fetching thanatopractitioners with page:", page);
await thanatopractitionerStore.fetchThanatopractitioners({
page,
per_page: 10,
});
} catch (error) {
console.error("Error changing page:", error);
notificationStore.error(
"Erreur de pagination",
"Une erreur est survenue lors du changement de page."
);
}
};
</script>