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
|
||||
:data="employeeData"
|
||||
:loading="loadingData"
|
||||
:pagination="pagination"
|
||||
@view="goToDetails"
|
||||
@delete="deleteEmployee"
|
||||
@changePage="handleChangePage"
|
||||
/>
|
||||
<!-- Pagination Component -->
|
||||
<div v-if="pagination && pagination.total > 0" class="mt-4 px-4">
|
||||
<nav aria-label="Employee pagination">
|
||||
<ul class="pagination justify-content-center mb-0">
|
||||
<li
|
||||
class="page-item"
|
||||
:class="{ disabled: pagination.current_page === 1 }"
|
||||
>
|
||||
<button
|
||||
class="page-link"
|
||||
@click="changePage(pagination.current_page - 1)"
|
||||
:disabled="pagination.current_page === 1"
|
||||
>
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
</button>
|
||||
</li>
|
||||
|
||||
<li
|
||||
v-for="page in visiblePages"
|
||||
:key="page"
|
||||
class="page-item"
|
||||
:class="{ active: page === pagination.current_page }"
|
||||
>
|
||||
<button class="page-link" @click="changePage(page)">
|
||||
{{ page }}
|
||||
</button>
|
||||
</li>
|
||||
|
||||
<li
|
||||
class="page-item"
|
||||
:class="{
|
||||
disabled: pagination.current_page === pagination.last_page,
|
||||
}"
|
||||
>
|
||||
<button
|
||||
class="page-link"
|
||||
@click="changePage(pagination.current_page + 1)"
|
||||
:disabled="pagination.current_page === pagination.last_page"
|
||||
>
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<!-- Pagination Info -->
|
||||
<div class="text-center mt-2">
|
||||
<small class="text-muted">
|
||||
Affichage de {{ pagination.from }} à {{ pagination.to }} sur
|
||||
{{ pagination.total }} employés
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</employee-template>
|
||||
</template>
|
||||
@ -78,13 +27,12 @@ import EmployeeTable from "@/components/molecules/Employees/EmployeeTable.vue";
|
||||
import addButton from "@/components/molecules/new-button/addButton.vue";
|
||||
import FilterTable from "@/components/molecules/Tables/FilterTable.vue";
|
||||
import TableAction from "@/components/molecules/Tables/TableAction.vue";
|
||||
import { computed } from "vue";
|
||||
import { defineProps, defineEmits } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const emit = defineEmits(["pushDetails", "deleteEmployee"]);
|
||||
const emit = defineEmits(["pushDetails", "deleteEmployee", "changePage"]);
|
||||
|
||||
const props = defineProps({
|
||||
employeeData: {
|
||||
@ -112,117 +60,25 @@ const goToDetails = (employeeId) => {
|
||||
};
|
||||
|
||||
const deleteEmployee = (employeeId) => {
|
||||
console.log(
|
||||
"deleteEmployee called in EmployeePresentation with ID:",
|
||||
employeeId
|
||||
);
|
||||
emit("deleteEmployee", employeeId);
|
||||
};
|
||||
|
||||
// Computed property for visible page numbers
|
||||
const visiblePages = computed(() => {
|
||||
if (!props.pagination) return [];
|
||||
|
||||
const current = props.pagination.current_page;
|
||||
const last = props.pagination.last_page;
|
||||
const delta = 2; // Number of pages to show on each side of current page
|
||||
|
||||
const range = [];
|
||||
const rangeWithDots = [];
|
||||
|
||||
// Calculate range around current page
|
||||
for (
|
||||
let i = Math.max(2, current - delta);
|
||||
i <= Math.min(last - 1, current + delta);
|
||||
i++
|
||||
) {
|
||||
range.push(i);
|
||||
}
|
||||
|
||||
// Add first page
|
||||
if (current - delta > 2) {
|
||||
rangeWithDots.push(1, "...");
|
||||
} else {
|
||||
rangeWithDots.push(1);
|
||||
}
|
||||
|
||||
// Add calculated range
|
||||
rangeWithDots.push(...range);
|
||||
|
||||
// Add last page
|
||||
if (current + delta < last - 1) {
|
||||
rangeWithDots.push("...", last);
|
||||
} else if (last > 1) {
|
||||
rangeWithDots.push(last);
|
||||
}
|
||||
|
||||
return rangeWithDots;
|
||||
});
|
||||
|
||||
const changePage = (page) => {
|
||||
if (page >= 1 && page <= props.pagination.last_page) {
|
||||
// Emit event for parent component to handle page change
|
||||
const handleChangePage = (page) => {
|
||||
console.log(
|
||||
"handleChangePage called in EmployeePresentation with page:",
|
||||
page
|
||||
);
|
||||
if (page >= 1 && page <= (props.pagination?.last_page || 1)) {
|
||||
console.log("Emitting changePage event from EmployeePresentation:", page);
|
||||
emit("changePage", page);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pagination {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.page-link {
|
||||
border: 1px solid #dee2e6;
|
||||
color: #5e72e4;
|
||||
text-decoration: none;
|
||||
background-color: #fff;
|
||||
transition: all 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.page-link:hover {
|
||||
color: #5e72e4;
|
||||
background-color: #e9ecef;
|
||||
border-color: #dee2e6;
|
||||
}
|
||||
|
||||
.page-item.active .page-link {
|
||||
background-color: #5e72e4;
|
||||
border-color: #5e72e4;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.page-item.disabled .page-link {
|
||||
color: #6c757d;
|
||||
background-color: #fff;
|
||||
border-color: #dee2e6;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.page-item:first-child .page-link {
|
||||
border-top-left-radius: 0.25rem;
|
||||
border-bottom-left-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.page-item:last-child .page-link {
|
||||
border-top-right-radius: 0.25rem;
|
||||
border-bottom-right-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.mt-4 {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.px-4 {
|
||||
padding-left: 1.5rem;
|
||||
padding-right: 1.5rem;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mb-0 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: #6c757d;
|
||||
}
|
||||
/* Component-specific styles */
|
||||
</style>
|
||||
|
||||
@ -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>
|
||||
|
||||
<!-- Data State -->
|
||||
<div v-else class="table-responsive">
|
||||
<table id="employee-list" class="table table-flush">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Nom & Prénom</th>
|
||||
<th>Email</th>
|
||||
<th>Téléphone</th>
|
||||
<th>Poste</th>
|
||||
<th>Date d'embauche</th>
|
||||
<th>Status</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="employee in data" :key="employee.id">
|
||||
<!-- ID Column -->
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<soft-checkbox />
|
||||
<p class="text-xs font-weight-bold ms-2 mb-0">
|
||||
{{ employee.id }}
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
<div v-else>
|
||||
<div class="table-responsive">
|
||||
<table id="employee-list" class="table table-flush">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Nom & Prénom</th>
|
||||
<th>Email</th>
|
||||
<th>Téléphone</th>
|
||||
<th>Poste</th>
|
||||
<th>Date d'embauche</th>
|
||||
<th>Status</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="employee in data" :key="employee.id">
|
||||
<!-- ID Column -->
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<soft-checkbox />
|
||||
<p class="text-xs font-weight-bold ms-2 mb-0">
|
||||
{{ employee.id }}
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Name Column -->
|
||||
<td class="font-weight-bold">
|
||||
<div class="d-flex align-items-center">
|
||||
<soft-avatar
|
||||
:img="getRandomAvatar()"
|
||||
size="xs"
|
||||
class="me-2"
|
||||
alt="user image"
|
||||
circular
|
||||
/>
|
||||
<div>
|
||||
<span
|
||||
>{{ employee.last_name }} {{ employee.first_name }}</span
|
||||
>
|
||||
<div
|
||||
v-if="employee.thanatopractitioner"
|
||||
class="text-xs text-info"
|
||||
>
|
||||
Thanatopractitioner
|
||||
<!-- Name Column -->
|
||||
<td class="font-weight-bold">
|
||||
<div class="d-flex align-items-center">
|
||||
<soft-avatar
|
||||
:img="getRandomAvatar()"
|
||||
size="xs"
|
||||
class="me-2"
|
||||
alt="user image"
|
||||
circular
|
||||
/>
|
||||
<div>
|
||||
<span
|
||||
>{{ employee.last_name }} {{ employee.first_name }}</span
|
||||
>
|
||||
<div
|
||||
v-if="employee.thanatopractitioner"
|
||||
class="text-xs text-info"
|
||||
>
|
||||
Thanatopractitioner
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</td>
|
||||
|
||||
<!-- Email Column -->
|
||||
<td class="text-xs font-weight-bold">
|
||||
<span class="text-xs">{{ employee.email || "N/A" }}</span>
|
||||
</td>
|
||||
<!-- Email Column -->
|
||||
<td class="text-xs font-weight-bold">
|
||||
<span class="text-xs">{{ employee.email || "N/A" }}</span>
|
||||
</td>
|
||||
|
||||
<!-- Phone Column -->
|
||||
<td class="text-xs font-weight-bold">
|
||||
<span class="text-xs">{{ employee.phone || "N/A" }}</span>
|
||||
</td>
|
||||
<!-- Phone Column -->
|
||||
<td class="text-xs font-weight-bold">
|
||||
<span class="text-xs">{{ employee.phone || "N/A" }}</span>
|
||||
</td>
|
||||
|
||||
<!-- Position Column -->
|
||||
<td class="text-xs font-weight-bold">
|
||||
<div class="d-flex align-items-center">
|
||||
<soft-button
|
||||
:color="getPositionColor(employee.job_title)"
|
||||
variant="outline"
|
||||
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
|
||||
>
|
||||
<i
|
||||
:class="getPositionIcon(employee.job_title)"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</soft-button>
|
||||
<span>{{ employee.job_title || "N/A" }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<!-- Position Column -->
|
||||
<td class="text-xs font-weight-bold">
|
||||
<div class="d-flex align-items-center">
|
||||
<soft-button
|
||||
:color="getPositionColor(employee.job_title)"
|
||||
variant="outline"
|
||||
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
|
||||
>
|
||||
<i
|
||||
:class="getPositionIcon(employee.job_title)"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</soft-button>
|
||||
<span>{{ employee.job_title || "N/A" }}</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Hire Date Column -->
|
||||
<td class="text-xs font-weight-bold">
|
||||
<span class="text-xs">{{ formatDate(employee.hire_date) }}</span>
|
||||
</td>
|
||||
<!-- Hire Date Column -->
|
||||
<td class="text-xs font-weight-bold">
|
||||
<span class="text-xs">{{
|
||||
formatDate(employee.hire_date)
|
||||
}}</span>
|
||||
</td>
|
||||
|
||||
<!-- Status Column -->
|
||||
<td class="text-xs font-weight-bold">
|
||||
<div class="d-flex align-items-center">
|
||||
<soft-button
|
||||
:color="employee.active ? 'success' : 'danger'"
|
||||
variant="outline"
|
||||
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
|
||||
>
|
||||
<i
|
||||
:class="employee.active ? 'fas fa-check' : 'fas fa-times'"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</soft-button>
|
||||
<span>{{ employee.active ? "Actif" : "Inactif" }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<!-- Status Column -->
|
||||
<td class="text-xs font-weight-bold">
|
||||
<div class="d-flex align-items-center">
|
||||
<soft-button
|
||||
:color="employee.active ? 'success' : 'danger'"
|
||||
variant="outline"
|
||||
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
|
||||
>
|
||||
<i
|
||||
:class="employee.active ? 'fas fa-check' : 'fas fa-times'"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</soft-button>
|
||||
<span>{{ employee.active ? "Actif" : "Inactif" }}</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<!-- View Button -->
|
||||
<soft-button
|
||||
color="info"
|
||||
variant="outline"
|
||||
title="Voir l'employé"
|
||||
:data-employee-id="employee.id"
|
||||
class="btn-icon-only btn-rounded mb-0 btn-sm d-flex align-items-center justify-content-center"
|
||||
>
|
||||
<i class="fas fa-eye" aria-hidden="true"></i>
|
||||
</soft-button>
|
||||
<td>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<!-- View Button -->
|
||||
<soft-button
|
||||
color="info"
|
||||
variant="outline"
|
||||
title="Voir l'employé"
|
||||
:data-employee-id="employee.id"
|
||||
class="btn-icon-only btn-rounded mb-0 btn-sm d-flex align-items-center justify-content-center"
|
||||
@click="handleView(employee.id)"
|
||||
>
|
||||
<i class="fas fa-eye" aria-hidden="true"></i>
|
||||
</soft-button>
|
||||
|
||||
<!-- Delete Button -->
|
||||
<soft-button
|
||||
color="danger"
|
||||
variant="outline"
|
||||
title="Supprimer l'employé"
|
||||
:data-employee-id="employee.id"
|
||||
class="btn-icon-only btn-rounded mb-0 btn-sm d-flex align-items-center justify-content-center"
|
||||
>
|
||||
<i class="fas fa-trash" aria-hidden="true"></i>
|
||||
</soft-button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- Delete Button -->
|
||||
<soft-button
|
||||
color="danger"
|
||||
variant="outline"
|
||||
title="Supprimer l'employé"
|
||||
:data-employee-id="employee.id"
|
||||
class="btn-icon-only btn-rounded mb-0 btn-sm d-flex align-items-center justify-content-center"
|
||||
@click="handleDelete(employee.id)"
|
||||
>
|
||||
<i class="fas fa-trash" aria-hidden="true"></i>
|
||||
</soft-button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Custom Pagination Controls -->
|
||||
<div
|
||||
v-if="pagination.total > pagination.per_page"
|
||||
class="d-flex justify-content-between align-items-center mt-3"
|
||||
>
|
||||
<div class="text-sm text-muted">
|
||||
Affichage de {{ pagination.from }} à {{ pagination.to }} sur
|
||||
{{ pagination.total }} employés
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<!-- Previous Button -->
|
||||
<soft-button
|
||||
color="outline"
|
||||
variant="outline"
|
||||
class="btn-sm"
|
||||
:disabled="pagination.current_page === 1 || loading"
|
||||
@click="changePage(pagination.current_page - 1)"
|
||||
>
|
||||
<i class="fas fa-chevron-left me-1"></i>
|
||||
Précédent
|
||||
</soft-button>
|
||||
|
||||
<!-- Page Numbers -->
|
||||
<div class="d-flex gap-1">
|
||||
<soft-button
|
||||
v-for="page in getVisiblePages()"
|
||||
:key="page"
|
||||
:color="page === pagination.current_page ? 'primary' : 'outline'"
|
||||
variant="outline"
|
||||
class="btn-sm"
|
||||
:disabled="loading"
|
||||
@click="changePage(page)"
|
||||
>
|
||||
{{ page }}
|
||||
</soft-button>
|
||||
</div>
|
||||
|
||||
<!-- Next Button -->
|
||||
<soft-button
|
||||
color="outline"
|
||||
variant="outline"
|
||||
class="btn-sm"
|
||||
:disabled="
|
||||
pagination.current_page === pagination.last_page || loading
|
||||
"
|
||||
@click="changePage(pagination.current_page + 1)"
|
||||
>
|
||||
Suivant
|
||||
<i class="fas fa-chevron-right ms-1"></i>
|
||||
</soft-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
@ -222,13 +281,13 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch, onUnmounted } from "vue";
|
||||
import { DataTable } from "simple-datatables";
|
||||
// import { DataTable } from "simple-datatables"; // Disabled to avoid interference
|
||||
import SoftCheckbox from "@/components/SoftCheckbox.vue";
|
||||
import SoftButton from "@/components/SoftButton.vue";
|
||||
import SoftAvatar from "@/components/SoftAvatar.vue";
|
||||
import { defineProps, defineEmits } from "vue";
|
||||
|
||||
const emit = defineEmits(["view", "delete"]);
|
||||
const emit = defineEmits(["view", "delete", "changePage"]);
|
||||
|
||||
// Sample avatar images
|
||||
import img1 from "@/assets/img/team-2.jpg";
|
||||
@ -240,8 +299,8 @@ import img6 from "@/assets/img/ivana-squares.jpg";
|
||||
|
||||
const avatarImages = [img1, img2, img3, img4, img5, img6];
|
||||
|
||||
// Reactive data
|
||||
const dataTableInstance = ref(null);
|
||||
// Reactive data - DataTable disabled
|
||||
// const dataTableInstance = ref(null);
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
@ -256,6 +315,17 @@ const props = defineProps({
|
||||
type: Number,
|
||||
default: 5,
|
||||
},
|
||||
pagination: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
current_page: 1,
|
||||
last_page: 1,
|
||||
per_page: 10,
|
||||
total: 0,
|
||||
from: 0,
|
||||
to: 0,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
// Methods
|
||||
@ -292,6 +362,67 @@ const getPositionIcon = (position) => {
|
||||
return icons[position] || "fas fa-user";
|
||||
};
|
||||
|
||||
// Direct button handlers
|
||||
const handleView = (employeeId) => {
|
||||
console.log("Direct view button clicked for ID:", employeeId);
|
||||
emit("view", employeeId);
|
||||
};
|
||||
|
||||
const handleDelete = (employeeId) => {
|
||||
console.log("Direct delete button clicked for ID:", employeeId);
|
||||
emit("delete", employeeId);
|
||||
};
|
||||
|
||||
// Pagination methods
|
||||
const changePage = (page) => {
|
||||
console.log("changePage called in EmployeeTable with page:", page);
|
||||
if (page >= 1 && page <= props.pagination.last_page) {
|
||||
console.log("Emitting changePage event from EmployeeTable:", page);
|
||||
emit("changePage", page);
|
||||
}
|
||||
};
|
||||
|
||||
const getVisiblePages = () => {
|
||||
const current = props.pagination.current_page;
|
||||
const last = props.pagination.last_page;
|
||||
const pages = [];
|
||||
|
||||
if (last <= 7) {
|
||||
// Show all pages if 7 or fewer
|
||||
for (let i = 1; i <= last; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
} else {
|
||||
// Show first page, current range, and last page
|
||||
pages.push(1);
|
||||
|
||||
if (current > 3) {
|
||||
pages.push("...");
|
||||
}
|
||||
|
||||
const start = Math.max(2, current - 1);
|
||||
const end = Math.min(last - 1, current + 1);
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
if (!pages.includes(i)) {
|
||||
pages.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
if (current < last - 2) {
|
||||
pages.push("...");
|
||||
}
|
||||
|
||||
if (!pages.includes(last)) {
|
||||
pages.push(last);
|
||||
}
|
||||
}
|
||||
|
||||
return pages;
|
||||
};
|
||||
|
||||
// Commented out DataTable initialization
|
||||
/*
|
||||
const initializeDataTable = () => {
|
||||
// Destroy existing instance if it exists
|
||||
if (dataTableInstance.value) {
|
||||
@ -301,62 +432,75 @@ const initializeDataTable = () => {
|
||||
|
||||
const dataTableEl = document.getElementById("employee-list");
|
||||
if (dataTableEl) {
|
||||
// Initialize DataTable with search and default pagination
|
||||
dataTableInstance.value = new DataTable(dataTableEl, {
|
||||
searchable: true,
|
||||
fixedHeight: true,
|
||||
perPage: 15,
|
||||
perPageSelect: [5, 10, 15, 20, 25],
|
||||
perPage: 10, // Default to 10 entries per page
|
||||
perPageSelect: false, // Disable per-page selector since we handle it server-side
|
||||
pager: false, // Disable DataTable pagination since we handle it server-side
|
||||
});
|
||||
|
||||
// Add click listener for action buttons
|
||||
dataTableEl.addEventListener("click", handleTableClick);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTableClick = (event) => {
|
||||
console.log("Table click detected:", event.target);
|
||||
const button = event.target.closest("button");
|
||||
if (!button) return;
|
||||
|
||||
const employeeId = button.getAttribute("data-employee-id");
|
||||
console.log("Employee ID:", employeeId);
|
||||
console.log("Button title:", button.title);
|
||||
|
||||
if (
|
||||
button.title === "Supprimer l'employé" ||
|
||||
button.querySelector(".fa-trash")
|
||||
) {
|
||||
console.log("Delete button clicked!");
|
||||
emit("delete", employeeId);
|
||||
} else if (
|
||||
button.title === "Voir l'employé" ||
|
||||
button.querySelector(".fa-eye")
|
||||
) {
|
||||
console.log("View button clicked!");
|
||||
emit("view", employeeId);
|
||||
}
|
||||
};
|
||||
*/
|
||||
|
||||
// Watch for data changes to reinitialize datatable
|
||||
// Watch for data changes
|
||||
watch(
|
||||
() => props.data,
|
||||
() => {
|
||||
if (!props.loading) {
|
||||
// Small delay to ensure DOM is updated
|
||||
setTimeout(() => {
|
||||
initializeDataTable();
|
||||
}, 100);
|
||||
console.log("EmployeeTable: Data changed");
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
const dataTableEl = document.getElementById("employee-list");
|
||||
if (dataTableEl) {
|
||||
dataTableEl.removeEventListener("click", handleTableClick);
|
||||
}
|
||||
if (dataTableInstance.value) {
|
||||
dataTableInstance.value.destroy();
|
||||
}
|
||||
// Clean up any event listeners if needed
|
||||
// const dataTableEl = document.getElementById("employee-list");
|
||||
// if (dataTableEl) {
|
||||
// dataTableEl.removeEventListener("click", handleTableClick);
|
||||
// }
|
||||
// if (dataTableInstance.value) {
|
||||
// dataTableInstance.value.destroy();
|
||||
// }
|
||||
});
|
||||
|
||||
// Initialize data
|
||||
onMounted(() => {
|
||||
if (!props.loading && props.data.length > 0) {
|
||||
initializeDataTable();
|
||||
console.log(
|
||||
"EmployeeTable: Component mounted with",
|
||||
props.data.length,
|
||||
"employees"
|
||||
);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -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",
|
||||
route: { name: "Employés Thanatopracteurs" },
|
||||
route: { name: "Gestion thanatopractitioners" },
|
||||
miniIcon: "T",
|
||||
text: "Employés & Thanatopracteurs",
|
||||
},
|
||||
|
||||
@ -529,6 +529,11 @@ const routes = [
|
||||
name: "Creation employé",
|
||||
component: () => import("@/views/pages/CRM/AddEmployee.vue"),
|
||||
},
|
||||
{
|
||||
path: "/employes/:id",
|
||||
name: "Employee details",
|
||||
component: () => import("@/views/pages/CRM/EmployeeDetails.vue"),
|
||||
},
|
||||
{
|
||||
path: "/employes/ndf",
|
||||
name: "NDF",
|
||||
@ -546,10 +551,24 @@ const routes = [
|
||||
},
|
||||
{
|
||||
path: "/employes/thanatopracteurs",
|
||||
name: "Employés Thanatopracteurs",
|
||||
name: "Gestion thanatopractitioners",
|
||||
component: () =>
|
||||
import("@/views/pages/Employes/EmployesThanatopracteurs.vue"),
|
||||
import("@/views/pages/Thanatopractitioners/Thanatopractitioners.vue"),
|
||||
},
|
||||
{
|
||||
path: "/employes/thanatopracteurs/new",
|
||||
name: "Creation thanatopractitioner",
|
||||
component: () =>
|
||||
import("@/views/pages/Thanatopractitioners/AddThanatopractitioner.vue"),
|
||||
},
|
||||
// {
|
||||
// path: "/employes/thanatopracteurs/:id",
|
||||
// name: "Thanatopractitioner details",
|
||||
// component: () =>
|
||||
// import(
|
||||
// "@/views/pages/Thanatopractitioners/ThanatopractitionerDetails.vue"
|
||||
// ),
|
||||
// },
|
||||
// Paramétrage
|
||||
{
|
||||
path: "/parametrage/droits",
|
||||
|
||||
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,
|
||||
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-data="employeeStore.employees"
|
||||
:loading-data="employeeStore.loading"
|
||||
:pagination="employeeStore.pagination"
|
||||
:pagination="employeeStore.getPagination"
|
||||
@push-details="goDetails"
|
||||
@delete-employee="deleteEmployee"
|
||||
@delete-employee="confirmDeleteEmployee"
|
||||
@change-page="changePage"
|
||||
/>
|
||||
|
||||
<!-- Confirm Delete Modal -->
|
||||
<confirm-modal
|
||||
:is-visible="confirmModal.isVisible"
|
||||
:title="confirmModal.title"
|
||||
:message="confirmModal.message"
|
||||
:details="confirmModal.details"
|
||||
:type="confirmModal.type"
|
||||
:confirm-text="confirmModal.confirmText"
|
||||
:cancel-text="confirmModal.cancelText"
|
||||
:is-loading="confirmModal.isLoading"
|
||||
@confirm="handleConfirmDelete"
|
||||
@cancel="handleCancelDelete"
|
||||
@close="handleCancelDelete"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, onMounted } from "vue";
|
||||
import EmployeePresentation from "@/components/Organism/Employee/EmployeePresentation.vue";
|
||||
import ConfirmModal from "@/components/molecules/common/ConfirmModal.vue";
|
||||
import { useEmployeeStore } from "@/stores/employeeStore";
|
||||
import { onMounted } from "vue";
|
||||
import { useNotificationStore } from "@/stores/notification";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
const employeeStore = useEmployeeStore();
|
||||
const notificationStore = useNotificationStore();
|
||||
const router = useRouter();
|
||||
|
||||
// Confirm modal state
|
||||
const confirmModal = reactive({
|
||||
isVisible: false,
|
||||
title: "Supprimer l'employé",
|
||||
message: "",
|
||||
details:
|
||||
"Cette action est irréversible. Toutes les données associées seront définitivement supprimées.",
|
||||
type: "danger",
|
||||
confirmText: "Supprimer",
|
||||
cancelText: "Annuler",
|
||||
isLoading: false,
|
||||
employeeId: null,
|
||||
employeeName: "",
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await employeeStore.fetchEmployees();
|
||||
});
|
||||
@ -30,24 +64,95 @@ const goDetails = (id) => {
|
||||
});
|
||||
};
|
||||
|
||||
const deleteEmployee = async (employeeId) => {
|
||||
try {
|
||||
if (confirm("Êtes-vous sûr de vouloir supprimer cet employé ?")) {
|
||||
await employeeStore.deleteEmployee(employeeId);
|
||||
alert("Employé supprimé avec succès");
|
||||
const confirmDeleteEmployee = (employeeId) => {
|
||||
console.log("confirmDeleteEmployee called with ID:", employeeId);
|
||||
console.log("Employee ID type:", typeof employeeId);
|
||||
console.log("Store employees:", employeeStore.employees);
|
||||
console.log("Store employees length:", employeeStore.employees.length);
|
||||
|
||||
const employee = employeeStore.employees.find((emp) => emp.id === employeeId);
|
||||
console.log("Found employee:", employee);
|
||||
|
||||
if (employee) {
|
||||
console.log("Showing confirmation modal");
|
||||
confirmModal.isVisible = true;
|
||||
confirmModal.employeeId = employeeId;
|
||||
confirmModal.employeeName = `${employee.first_name} ${employee.last_name}`;
|
||||
confirmModal.message = `Êtes-vous sûr de vouloir supprimer l'employé "${confirmModal.employeeName}" ?`;
|
||||
} else {
|
||||
console.log("No employee found - trying to parse as number");
|
||||
const numericId = parseInt(employeeId, 10);
|
||||
console.log("Trying with numeric ID:", numericId);
|
||||
const employeeByNumber = employeeStore.employees.find(
|
||||
(emp) => emp.id === numericId
|
||||
);
|
||||
console.log("Found by numeric ID:", employeeByNumber);
|
||||
|
||||
if (employeeByNumber) {
|
||||
console.log("Showing confirmation modal with numeric ID");
|
||||
confirmModal.isVisible = true;
|
||||
confirmModal.employeeId = numericId;
|
||||
confirmModal.employeeName = `${employeeByNumber.first_name} ${employeeByNumber.last_name}`;
|
||||
confirmModal.message = `Êtes-vous sûr de vouloir supprimer l'employé "${confirmModal.employeeName}" ?`;
|
||||
} else {
|
||||
console.log(
|
||||
"Still no employee found - displaying all employee IDs for comparison"
|
||||
);
|
||||
employeeStore.employees.forEach((emp, index) => {
|
||||
console.log(
|
||||
`Employee ${index}: ID=${emp.id} (${typeof emp.id}), Name=${
|
||||
emp.first_name
|
||||
} ${emp.last_name}`
|
||||
);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error deleting employee:", error);
|
||||
alert("Erreur lors de la suppression de l'employé");
|
||||
}
|
||||
};
|
||||
|
||||
const changePage = async (page) => {
|
||||
const handleConfirmDelete = async () => {
|
||||
const employeeId = confirmModal.employeeId;
|
||||
const employeeName = confirmModal.employeeName;
|
||||
console.log("Test");
|
||||
try {
|
||||
await employeeStore.fetchEmployees({ page });
|
||||
confirmModal.isLoading = true;
|
||||
await employeeStore.deleteEmployee(employeeId);
|
||||
notificationStore.success(
|
||||
"Employé supprimé",
|
||||
`L'employé ${employeeName} a été supprimé avec succès.`
|
||||
);
|
||||
closeConfirmModal();
|
||||
} catch (error) {
|
||||
console.error("Error deleting employee:", error);
|
||||
notificationStore.error(
|
||||
"Erreur de suppression",
|
||||
"Une erreur est survenue lors de la suppression de l'employé."
|
||||
);
|
||||
closeConfirmModal();
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelDelete = () => {
|
||||
closeConfirmModal();
|
||||
};
|
||||
|
||||
const closeConfirmModal = () => {
|
||||
confirmModal.isVisible = false;
|
||||
confirmModal.employeeId = null;
|
||||
confirmModal.employeeName = "";
|
||||
confirmModal.isLoading = false;
|
||||
};
|
||||
|
||||
const changePage = async (page) => {
|
||||
console.log("changePage called in Employees.vue with page:", page);
|
||||
try {
|
||||
console.log("Fetching employees with page:", page);
|
||||
await employeeStore.fetchEmployees({ page, per_page: 10 });
|
||||
} catch (error) {
|
||||
console.error("Error changing page:", error);
|
||||
alert("Erreur lors du changement de page");
|
||||
notificationStore.error(
|
||||
"Erreur de pagination",
|
||||
"Une erreur est survenue lors du changement de page."
|
||||
);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -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