add gestion thanato
This commit is contained in:
parent
e55cc5253e
commit
8d1d65e27b
@ -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>
|
||||||
@ -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>
|
||||||
@ -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>
|
||||||
@ -13,62 +13,11 @@
|
|||||||
<employee-table
|
<employee-table
|
||||||
:data="employeeData"
|
:data="employeeData"
|
||||||
:loading="loadingData"
|
:loading="loadingData"
|
||||||
|
:pagination="pagination"
|
||||||
@view="goToDetails"
|
@view="goToDetails"
|
||||||
@delete="deleteEmployee"
|
@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>
|
</template>
|
||||||
</employee-template>
|
</employee-template>
|
||||||
</template>
|
</template>
|
||||||
@ -78,13 +27,12 @@ import EmployeeTable from "@/components/molecules/Employees/EmployeeTable.vue";
|
|||||||
import addButton from "@/components/molecules/new-button/addButton.vue";
|
import addButton from "@/components/molecules/new-button/addButton.vue";
|
||||||
import FilterTable from "@/components/molecules/Tables/FilterTable.vue";
|
import FilterTable from "@/components/molecules/Tables/FilterTable.vue";
|
||||||
import TableAction from "@/components/molecules/Tables/TableAction.vue";
|
import TableAction from "@/components/molecules/Tables/TableAction.vue";
|
||||||
import { computed } from "vue";
|
|
||||||
import { defineProps, defineEmits } from "vue";
|
import { defineProps, defineEmits } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const emit = defineEmits(["pushDetails", "deleteEmployee"]);
|
const emit = defineEmits(["pushDetails", "deleteEmployee", "changePage"]);
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
employeeData: {
|
employeeData: {
|
||||||
@ -112,117 +60,25 @@ const goToDetails = (employeeId) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const deleteEmployee = (employeeId) => {
|
const deleteEmployee = (employeeId) => {
|
||||||
|
console.log(
|
||||||
|
"deleteEmployee called in EmployeePresentation with ID:",
|
||||||
|
employeeId
|
||||||
|
);
|
||||||
emit("deleteEmployee", employeeId);
|
emit("deleteEmployee", employeeId);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Computed property for visible page numbers
|
const handleChangePage = (page) => {
|
||||||
const visiblePages = computed(() => {
|
console.log(
|
||||||
if (!props.pagination) return [];
|
"handleChangePage called in EmployeePresentation with page:",
|
||||||
|
page
|
||||||
const current = props.pagination.current_page;
|
);
|
||||||
const last = props.pagination.last_page;
|
if (page >= 1 && page <= (props.pagination?.last_page || 1)) {
|
||||||
const delta = 2; // Number of pages to show on each side of current page
|
console.log("Emitting changePage event from EmployeePresentation:", 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
|
|
||||||
emit("changePage", page);
|
emit("changePage", page);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.pagination {
|
/* Component-specific styles */
|
||||||
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;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -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>
|
||||||
@ -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>
|
||||||
@ -78,133 +78,192 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Data State -->
|
<!-- Data State -->
|
||||||
<div v-else class="table-responsive">
|
<div v-else>
|
||||||
<table id="employee-list" class="table table-flush">
|
<div class="table-responsive">
|
||||||
<thead class="thead-light">
|
<table id="employee-list" class="table table-flush">
|
||||||
<tr>
|
<thead class="thead-light">
|
||||||
<th>ID</th>
|
<tr>
|
||||||
<th>Nom & Prénom</th>
|
<th>ID</th>
|
||||||
<th>Email</th>
|
<th>Nom & Prénom</th>
|
||||||
<th>Téléphone</th>
|
<th>Email</th>
|
||||||
<th>Poste</th>
|
<th>Téléphone</th>
|
||||||
<th>Date d'embauche</th>
|
<th>Poste</th>
|
||||||
<th>Status</th>
|
<th>Date d'embauche</th>
|
||||||
<th>Action</th>
|
<th>Status</th>
|
||||||
</tr>
|
<th>Action</th>
|
||||||
</thead>
|
</tr>
|
||||||
<tbody>
|
</thead>
|
||||||
<tr v-for="employee in data" :key="employee.id">
|
<tbody>
|
||||||
<!-- ID Column -->
|
<tr v-for="employee in data" :key="employee.id">
|
||||||
<td>
|
<!-- ID Column -->
|
||||||
<div class="d-flex align-items-center">
|
<td>
|
||||||
<soft-checkbox />
|
<div class="d-flex align-items-center">
|
||||||
<p class="text-xs font-weight-bold ms-2 mb-0">
|
<soft-checkbox />
|
||||||
{{ employee.id }}
|
<p class="text-xs font-weight-bold ms-2 mb-0">
|
||||||
</p>
|
{{ employee.id }}
|
||||||
</div>
|
</p>
|
||||||
</td>
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
<!-- Name Column -->
|
<!-- Name Column -->
|
||||||
<td class="font-weight-bold">
|
<td class="font-weight-bold">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<soft-avatar
|
<soft-avatar
|
||||||
:img="getRandomAvatar()"
|
:img="getRandomAvatar()"
|
||||||
size="xs"
|
size="xs"
|
||||||
class="me-2"
|
class="me-2"
|
||||||
alt="user image"
|
alt="user image"
|
||||||
circular
|
circular
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<span
|
<span
|
||||||
>{{ employee.last_name }} {{ employee.first_name }}</span
|
>{{ employee.last_name }} {{ employee.first_name }}</span
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="employee.thanatopractitioner"
|
v-if="employee.thanatopractitioner"
|
||||||
class="text-xs text-info"
|
class="text-xs text-info"
|
||||||
>
|
>
|
||||||
Thanatopractitioner
|
Thanatopractitioner
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</td>
|
||||||
</td>
|
|
||||||
|
|
||||||
<!-- Email Column -->
|
<!-- Email Column -->
|
||||||
<td class="text-xs font-weight-bold">
|
<td class="text-xs font-weight-bold">
|
||||||
<span class="text-xs">{{ employee.email || "N/A" }}</span>
|
<span class="text-xs">{{ employee.email || "N/A" }}</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<!-- Phone Column -->
|
<!-- Phone Column -->
|
||||||
<td class="text-xs font-weight-bold">
|
<td class="text-xs font-weight-bold">
|
||||||
<span class="text-xs">{{ employee.phone || "N/A" }}</span>
|
<span class="text-xs">{{ employee.phone || "N/A" }}</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<!-- Position Column -->
|
<!-- Position Column -->
|
||||||
<td class="text-xs font-weight-bold">
|
<td class="text-xs font-weight-bold">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<soft-button
|
<soft-button
|
||||||
:color="getPositionColor(employee.job_title)"
|
:color="getPositionColor(employee.job_title)"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
|
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
|
||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
:class="getPositionIcon(employee.job_title)"
|
:class="getPositionIcon(employee.job_title)"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
></i>
|
></i>
|
||||||
</soft-button>
|
</soft-button>
|
||||||
<span>{{ employee.job_title || "N/A" }}</span>
|
<span>{{ employee.job_title || "N/A" }}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<!-- Hire Date Column -->
|
<!-- Hire Date Column -->
|
||||||
<td class="text-xs font-weight-bold">
|
<td class="text-xs font-weight-bold">
|
||||||
<span class="text-xs">{{ formatDate(employee.hire_date) }}</span>
|
<span class="text-xs">{{
|
||||||
</td>
|
formatDate(employee.hire_date)
|
||||||
|
}}</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
<!-- Status Column -->
|
<!-- Status Column -->
|
||||||
<td class="text-xs font-weight-bold">
|
<td class="text-xs font-weight-bold">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<soft-button
|
<soft-button
|
||||||
:color="employee.active ? 'success' : 'danger'"
|
:color="employee.active ? 'success' : 'danger'"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
|
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
|
||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
:class="employee.active ? 'fas fa-check' : 'fas fa-times'"
|
:class="employee.active ? 'fas fa-check' : 'fas fa-times'"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
></i>
|
></i>
|
||||||
</soft-button>
|
</soft-button>
|
||||||
<span>{{ employee.active ? "Actif" : "Inactif" }}</span>
|
<span>{{ employee.active ? "Actif" : "Inactif" }}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-center gap-2">
|
||||||
<!-- View Button -->
|
<!-- View Button -->
|
||||||
<soft-button
|
<soft-button
|
||||||
color="info"
|
color="info"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
title="Voir l'employé"
|
title="Voir l'employé"
|
||||||
:data-employee-id="employee.id"
|
:data-employee-id="employee.id"
|
||||||
class="btn-icon-only btn-rounded mb-0 btn-sm d-flex align-items-center justify-content-center"
|
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>
|
<i class="fas fa-eye" aria-hidden="true"></i>
|
||||||
|
</soft-button>
|
||||||
|
|
||||||
<!-- Delete Button -->
|
<!-- Delete Button -->
|
||||||
<soft-button
|
<soft-button
|
||||||
color="danger"
|
color="danger"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
title="Supprimer l'employé"
|
title="Supprimer l'employé"
|
||||||
:data-employee-id="employee.id"
|
:data-employee-id="employee.id"
|
||||||
class="btn-icon-only btn-rounded mb-0 btn-sm d-flex align-items-center justify-content-center"
|
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>
|
<i class="fas fa-trash" aria-hidden="true"></i>
|
||||||
</div>
|
</soft-button>
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
</td>
|
||||||
</tbody>
|
</tr>
|
||||||
</table>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Empty State -->
|
<!-- Empty State -->
|
||||||
@ -222,13 +281,13 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, watch, onUnmounted } from "vue";
|
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 SoftCheckbox from "@/components/SoftCheckbox.vue";
|
||||||
import SoftButton from "@/components/SoftButton.vue";
|
import SoftButton from "@/components/SoftButton.vue";
|
||||||
import SoftAvatar from "@/components/SoftAvatar.vue";
|
import SoftAvatar from "@/components/SoftAvatar.vue";
|
||||||
import { defineProps, defineEmits } from "vue";
|
import { defineProps, defineEmits } from "vue";
|
||||||
|
|
||||||
const emit = defineEmits(["view", "delete"]);
|
const emit = defineEmits(["view", "delete", "changePage"]);
|
||||||
|
|
||||||
// Sample avatar images
|
// Sample avatar images
|
||||||
import img1 from "@/assets/img/team-2.jpg";
|
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];
|
const avatarImages = [img1, img2, img3, img4, img5, img6];
|
||||||
|
|
||||||
// Reactive data
|
// Reactive data - DataTable disabled
|
||||||
const dataTableInstance = ref(null);
|
// const dataTableInstance = ref(null);
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
data: {
|
data: {
|
||||||
@ -256,6 +315,17 @@ const props = defineProps({
|
|||||||
type: Number,
|
type: Number,
|
||||||
default: 5,
|
default: 5,
|
||||||
},
|
},
|
||||||
|
pagination: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({
|
||||||
|
current_page: 1,
|
||||||
|
last_page: 1,
|
||||||
|
per_page: 10,
|
||||||
|
total: 0,
|
||||||
|
from: 0,
|
||||||
|
to: 0,
|
||||||
|
}),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
@ -292,6 +362,67 @@ const getPositionIcon = (position) => {
|
|||||||
return icons[position] || "fas fa-user";
|
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 = () => {
|
const initializeDataTable = () => {
|
||||||
// Destroy existing instance if it exists
|
// Destroy existing instance if it exists
|
||||||
if (dataTableInstance.value) {
|
if (dataTableInstance.value) {
|
||||||
@ -301,62 +432,75 @@ const initializeDataTable = () => {
|
|||||||
|
|
||||||
const dataTableEl = document.getElementById("employee-list");
|
const dataTableEl = document.getElementById("employee-list");
|
||||||
if (dataTableEl) {
|
if (dataTableEl) {
|
||||||
|
// Initialize DataTable with search and default pagination
|
||||||
dataTableInstance.value = new DataTable(dataTableEl, {
|
dataTableInstance.value = new DataTable(dataTableEl, {
|
||||||
searchable: true,
|
searchable: true,
|
||||||
fixedHeight: true,
|
fixedHeight: true,
|
||||||
perPage: 15,
|
perPage: 10, // Default to 10 entries per page
|
||||||
perPageSelect: [5, 10, 15, 20, 25],
|
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);
|
dataTableEl.addEventListener("click", handleTableClick);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTableClick = (event) => {
|
const handleTableClick = (event) => {
|
||||||
|
console.log("Table click detected:", event.target);
|
||||||
const button = event.target.closest("button");
|
const button = event.target.closest("button");
|
||||||
if (!button) return;
|
if (!button) return;
|
||||||
|
|
||||||
const employeeId = button.getAttribute("data-employee-id");
|
const employeeId = button.getAttribute("data-employee-id");
|
||||||
|
console.log("Employee ID:", employeeId);
|
||||||
|
console.log("Button title:", button.title);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
button.title === "Supprimer l'employé" ||
|
button.title === "Supprimer l'employé" ||
|
||||||
button.querySelector(".fa-trash")
|
button.querySelector(".fa-trash")
|
||||||
) {
|
) {
|
||||||
|
console.log("Delete button clicked!");
|
||||||
emit("delete", employeeId);
|
emit("delete", employeeId);
|
||||||
} else if (
|
} else if (
|
||||||
button.title === "Voir l'employé" ||
|
button.title === "Voir l'employé" ||
|
||||||
button.querySelector(".fa-eye")
|
button.querySelector(".fa-eye")
|
||||||
) {
|
) {
|
||||||
|
console.log("View button clicked!");
|
||||||
emit("view", employeeId);
|
emit("view", employeeId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
*/
|
||||||
|
|
||||||
// Watch for data changes to reinitialize datatable
|
// Watch for data changes
|
||||||
watch(
|
watch(
|
||||||
() => props.data,
|
() => props.data,
|
||||||
() => {
|
() => {
|
||||||
if (!props.loading) {
|
if (!props.loading) {
|
||||||
// Small delay to ensure DOM is updated
|
console.log("EmployeeTable: Data changed");
|
||||||
setTimeout(() => {
|
|
||||||
initializeDataTable();
|
|
||||||
}, 100);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ deep: true }
|
{ deep: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
const dataTableEl = document.getElementById("employee-list");
|
// Clean up any event listeners if needed
|
||||||
if (dataTableEl) {
|
// const dataTableEl = document.getElementById("employee-list");
|
||||||
dataTableEl.removeEventListener("click", handleTableClick);
|
// if (dataTableEl) {
|
||||||
}
|
// dataTableEl.removeEventListener("click", handleTableClick);
|
||||||
if (dataTableInstance.value) {
|
// }
|
||||||
dataTableInstance.value.destroy();
|
// if (dataTableInstance.value) {
|
||||||
}
|
// dataTableInstance.value.destroy();
|
||||||
|
// }
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize data
|
// Initialize data
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!props.loading && props.data.length > 0) {
|
if (!props.loading && props.data.length > 0) {
|
||||||
initializeDataTable();
|
console.log(
|
||||||
|
"EmployeeTable: Component mounted with",
|
||||||
|
props.data.length,
|
||||||
|
"employees"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -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>
|
||||||
431
thanasoft-front/src/components/molecules/common/ConfirmModal.vue
Normal file
431
thanasoft-front/src/components/molecules/common/ConfirmModal.vue
Normal 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>
|
||||||
@ -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>
|
||||||
@ -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>
|
||||||
@ -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>
|
||||||
@ -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>
|
||||||
@ -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>
|
||||||
@ -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>
|
||||||
@ -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>
|
||||||
@ -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>
|
||||||
@ -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>
|
||||||
271
thanasoft-front/src/examples/ConfirmModalUsage.vue
Normal file
271
thanasoft-front/src/examples/ConfirmModalUsage.vue
Normal 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>
|
||||||
@ -308,7 +308,7 @@ export default {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "employes-thanatopracteurs",
|
id: "employes-thanatopracteurs",
|
||||||
route: { name: "Employés Thanatopracteurs" },
|
route: { name: "Gestion thanatopractitioners" },
|
||||||
miniIcon: "T",
|
miniIcon: "T",
|
||||||
text: "Employés & Thanatopracteurs",
|
text: "Employés & Thanatopracteurs",
|
||||||
},
|
},
|
||||||
|
|||||||
@ -529,6 +529,11 @@ const routes = [
|
|||||||
name: "Creation employé",
|
name: "Creation employé",
|
||||||
component: () => import("@/views/pages/CRM/AddEmployee.vue"),
|
component: () => import("@/views/pages/CRM/AddEmployee.vue"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/employes/:id",
|
||||||
|
name: "Employee details",
|
||||||
|
component: () => import("@/views/pages/CRM/EmployeeDetails.vue"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/employes/ndf",
|
path: "/employes/ndf",
|
||||||
name: "NDF",
|
name: "NDF",
|
||||||
@ -546,10 +551,24 @@ const routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/employes/thanatopracteurs",
|
path: "/employes/thanatopracteurs",
|
||||||
name: "Employés Thanatopracteurs",
|
name: "Gestion thanatopractitioners",
|
||||||
component: () =>
|
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
|
// Paramétrage
|
||||||
{
|
{
|
||||||
path: "/parametrage/droits",
|
path: "/parametrage/droits",
|
||||||
|
|||||||
286
thanasoft-front/src/services/thanatopractitioner.ts
Normal file
286
thanasoft-front/src/services/thanatopractitioner.ts
Normal 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;
|
||||||
@ -79,6 +79,16 @@ export const useEmployeeStore = defineStore("employee", () => {
|
|||||||
from: meta.from || 0,
|
from: meta.from || 0,
|
||||||
to: meta.to || 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,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
421
thanasoft-front/src/stores/thanatopractitionerStore.ts
Normal file
421
thanasoft-front/src/stores/thanatopractitionerStore.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
107
thanasoft-front/src/views/pages/CRM/EmployeeDetails.vue
Normal file
107
thanasoft-front/src/views/pages/CRM/EmployeeDetails.vue
Normal 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>
|
||||||
@ -2,21 +2,55 @@
|
|||||||
<employee-presentation
|
<employee-presentation
|
||||||
:employee-data="employeeStore.employees"
|
:employee-data="employeeStore.employees"
|
||||||
:loading-data="employeeStore.loading"
|
:loading-data="employeeStore.loading"
|
||||||
:pagination="employeeStore.pagination"
|
:pagination="employeeStore.getPagination"
|
||||||
@push-details="goDetails"
|
@push-details="goDetails"
|
||||||
@delete-employee="deleteEmployee"
|
@delete-employee="confirmDeleteEmployee"
|
||||||
@change-page="changePage"
|
@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>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { reactive, onMounted } from "vue";
|
||||||
import EmployeePresentation from "@/components/Organism/Employee/EmployeePresentation.vue";
|
import EmployeePresentation from "@/components/Organism/Employee/EmployeePresentation.vue";
|
||||||
|
import ConfirmModal from "@/components/molecules/common/ConfirmModal.vue";
|
||||||
import { useEmployeeStore } from "@/stores/employeeStore";
|
import { useEmployeeStore } from "@/stores/employeeStore";
|
||||||
import { onMounted } from "vue";
|
import { useNotificationStore } from "@/stores/notification";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
const employeeStore = useEmployeeStore();
|
const employeeStore = useEmployeeStore();
|
||||||
|
const notificationStore = useNotificationStore();
|
||||||
const router = useRouter();
|
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 () => {
|
onMounted(async () => {
|
||||||
await employeeStore.fetchEmployees();
|
await employeeStore.fetchEmployees();
|
||||||
});
|
});
|
||||||
@ -30,24 +64,95 @@ const goDetails = (id) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteEmployee = async (employeeId) => {
|
const confirmDeleteEmployee = (employeeId) => {
|
||||||
try {
|
console.log("confirmDeleteEmployee called with ID:", employeeId);
|
||||||
if (confirm("Êtes-vous sûr de vouloir supprimer cet employé ?")) {
|
console.log("Employee ID type:", typeof employeeId);
|
||||||
await employeeStore.deleteEmployee(employeeId);
|
console.log("Store employees:", employeeStore.employees);
|
||||||
alert("Employé supprimé avec succès");
|
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 {
|
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) {
|
} catch (error) {
|
||||||
console.error("Error changing page:", 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>
|
</script>
|
||||||
|
|||||||
@ -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>
|
||||||
@ -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>
|
||||||
Loading…
x
Reference in New Issue
Block a user