688 lines
18 KiB
Vue
Executable File
688 lines
18 KiB
Vue
Executable File
<template>
|
||
<div :class="['cell', { expanded }]" @click="handleClick">
|
||
<h3 class="fs-5 fw-medium">{{ titleText }}</h3>
|
||
|
||
<div class="tooltip-explain">{{ description }}</div>
|
||
|
||
<template v-if="expanded">
|
||
<div class="explain">
|
||
<p>{{ description }}</p>
|
||
</div>
|
||
</template>
|
||
|
||
<div class="description-wrapper custom-flow">
|
||
<div
|
||
v-for="(desc, index) in currentDescriptions"
|
||
:key="desc.idSectionCell || index"
|
||
:class="[
|
||
'section-bloc',
|
||
index % 2 === 0 ? 'from-left' : 'from-right',
|
||
]"
|
||
>
|
||
<!-- ADMIN -------------------------------------------------------------------------------------------->
|
||
<template v-if="IS_ADMIN">
|
||
<div class="description">
|
||
<p class="m-0">{{ desc.contentSectionCell }}</p>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- ENTREP ------------------------------------------------------------------------------------------->
|
||
<template v-else>
|
||
<!-- Mode affichage -->
|
||
<template v-if="!isEditing[index]">
|
||
<template v-if="expanded">
|
||
<button
|
||
class="delete-button"
|
||
title="Supprimer"
|
||
@click.stop="
|
||
deleteSectionCell(desc.idSectionCell, index)
|
||
"
|
||
>
|
||
✕
|
||
</button>
|
||
</template>
|
||
|
||
<div class="description">
|
||
<p class="m-0">{{ desc.contentSectionCell }}</p>
|
||
</div>
|
||
<div class="button-container">
|
||
<button
|
||
v-if="expanded"
|
||
class="edit-button"
|
||
@click.stop="startEditing(index)"
|
||
>
|
||
Éditer
|
||
</button>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Mode édition -->
|
||
<template v-else>
|
||
<div class="edit-row">
|
||
<textarea
|
||
v-model="
|
||
editedDescriptions[index].contentSectionCell
|
||
"
|
||
class="edit-input"
|
||
></textarea>
|
||
<div class="button-container">
|
||
<button
|
||
class="save-button"
|
||
@click.stop="saveEdit(index)"
|
||
>
|
||
Enregistrer
|
||
</button>
|
||
<button
|
||
class="cancel-button"
|
||
@click.stop="cancelEdit(index)"
|
||
>
|
||
Annuler
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</template>
|
||
</div>
|
||
|
||
<template v-if="expanded">
|
||
<div class="canvas-exit-hint">
|
||
Cliquez n'importe où pour quitter le canvas (terminez
|
||
d'abord vos modifications)
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, defineProps, onMounted } from "vue";
|
||
import { getSectionCellsByDate } from "@/services/Apis/Shared.ts";
|
||
import { addSectionCell } from "@/services/Apis/Entrepreneurs.ts";
|
||
import SectionCell from "@/ApiClasses/SectionCell";
|
||
import { removeSectionCell } from "@/services/Apis/Entrepreneurs.ts";
|
||
import type { AxiosResponse, AxiosError } from "axios";
|
||
|
||
const props = defineProps<{
|
||
projectId: number;
|
||
title: number;
|
||
titleText: string;
|
||
description: string;
|
||
isAdmin: boolean;
|
||
}>();
|
||
|
||
const IS_MOCK_MODE = true;
|
||
const IS_ADMIN = props.isAdmin;
|
||
|
||
const expanded = ref(false);
|
||
const currentDescriptions = ref<SectionCell[]>([]);
|
||
const editedDescriptions = ref<SectionCell[]>([]);
|
||
const isEditing = ref<boolean[]>([]);
|
||
|
||
const startEditing = (index: number) => {
|
||
isEditing.value[index] = true;
|
||
};
|
||
|
||
const cancelEdit = (index: number) => {
|
||
editedDescriptions.value[index].contentSectionCell =
|
||
currentDescriptions.value[index].contentSectionCell;
|
||
isEditing.value[index] = false;
|
||
};
|
||
|
||
const saveEdit = (index: number) => {
|
||
currentDescriptions.value[index].contentSectionCell =
|
||
editedDescriptions.value[index].contentSectionCell;
|
||
isEditing.value[index] = false;
|
||
|
||
if (!IS_MOCK_MODE) {
|
||
addSectionCell(
|
||
currentDescriptions.value[index],
|
||
(response) => {
|
||
console.log(
|
||
"Modification enregistrée avec succès :",
|
||
response.data
|
||
);
|
||
},
|
||
(error) => {
|
||
console.error("Erreur lors de l'enregistrement :", error);
|
||
}
|
||
);
|
||
}
|
||
};
|
||
|
||
const deleteSectionCell = (id: number | undefined, index: number): void => {
|
||
if (id === -1) {
|
||
window.confirm("Êtes-vous sûr de vouloir supprimer cet élément ?");
|
||
console.error("Delete ignored");
|
||
return;
|
||
}
|
||
|
||
const confirmed = window.confirm(
|
||
"Êtes-vous sûr de vouloir supprimer cet élément ?"
|
||
);
|
||
|
||
if (!confirmed) return;
|
||
|
||
if (id === undefined) {
|
||
console.error("ID de la cellule non défini");
|
||
return;
|
||
}
|
||
|
||
removeSectionCell(
|
||
id,
|
||
() => {
|
||
currentDescriptions.value.splice(index, 1);
|
||
editedDescriptions.value.splice(index, 1);
|
||
isEditing.value.splice(index, 1);
|
||
},
|
||
(error: AxiosError) => {
|
||
console.error("Erreur lors de la suppression :", error);
|
||
}
|
||
);
|
||
};
|
||
|
||
const handleClick = () => {
|
||
if (expanded.value) {
|
||
const editingInProgress = isEditing.value.some((edit) => edit);
|
||
if (!editingInProgress) {
|
||
expanded.value = false;
|
||
}
|
||
} else {
|
||
expanded.value = true;
|
||
}
|
||
};
|
||
|
||
const handleFetchSuccess = (sectionCells: SectionCell[]) => {
|
||
currentDescriptions.value = sectionCells;
|
||
editedDescriptions.value = sectionCells.map(
|
||
(cell) =>
|
||
new SectionCell({
|
||
idSectionCell: cell.idSectionCell,
|
||
sectionId: cell.sectionId,
|
||
contentSectionCell: cell.contentSectionCell,
|
||
modificationDate: cell.modificationDate,
|
||
})
|
||
);
|
||
isEditing.value = Array(sectionCells.length).fill(false);
|
||
};
|
||
|
||
const handleFetchError = (error: unknown) => {
|
||
console.error("Erreur lors de la récupération des données :", error);
|
||
|
||
const errorCell = new SectionCell({
|
||
idSectionCell: -1,
|
||
sectionId: -1,
|
||
contentSectionCell: "Échec du chargement des données.",
|
||
modificationDate: new Date().toISOString(),
|
||
});
|
||
|
||
currentDescriptions.value = [errorCell];
|
||
editedDescriptions.value = [errorCell];
|
||
isEditing.value = [false];
|
||
};
|
||
|
||
const fetchData = async (
|
||
projectId: number,
|
||
title: number,
|
||
date: string,
|
||
useMock = false
|
||
) => {
|
||
try {
|
||
if (useMock) {
|
||
const responseData = await mockFetch(projectId, title, date);
|
||
handleFetchSuccess(responseData);
|
||
} else {
|
||
if (projectId == -1) {
|
||
const errorCell = new SectionCell({
|
||
idSectionCell: -1,
|
||
sectionId: -1,
|
||
contentSectionCell: "Échec du chargement des données.",
|
||
modificationDate: new Date().toISOString(),
|
||
});
|
||
|
||
currentDescriptions.value = [errorCell];
|
||
editedDescriptions.value = [errorCell];
|
||
isEditing.value = [false];
|
||
|
||
console.error(
|
||
"No sections to show because no project was found."
|
||
);
|
||
return;
|
||
}
|
||
await new Promise<void>((resolve, reject) => {
|
||
getSectionCellsByDate(
|
||
projectId,
|
||
title,
|
||
date,
|
||
(response: AxiosResponse) => {
|
||
const data = response.data;
|
||
|
||
if (Array.isArray(data) && data.length > 0) {
|
||
const sectionCells = data.map(
|
||
(cellData) =>
|
||
new SectionCell({
|
||
idSectionCell: cellData.idSectionCell,
|
||
sectionId: cellData.sectionId,
|
||
contentSectionCell:
|
||
cellData.contentSectionCell,
|
||
modificationDate:
|
||
cellData.modificationDate,
|
||
})
|
||
);
|
||
handleFetchSuccess(sectionCells);
|
||
} else {
|
||
console.warn(
|
||
"Aucune donnée reçue ou format inattendu :",
|
||
data
|
||
);
|
||
}
|
||
|
||
resolve();
|
||
},
|
||
(error: AxiosError) => {
|
||
handleFetchError(error);
|
||
reject(error);
|
||
}
|
||
);
|
||
});
|
||
}
|
||
} catch (error) {
|
||
handleFetchError(error);
|
||
}
|
||
};
|
||
|
||
const mockFetch = async (
|
||
projectId: number,
|
||
title: number,
|
||
date: string
|
||
): Promise<SectionCell[]> => {
|
||
console.log(
|
||
`Mock fetch pour projectId: ${projectId}, title: ${title}, date: ${date}`
|
||
);
|
||
|
||
const leanCanvasData: Record<number, string[]> = {
|
||
1: [
|
||
"Les clients ont du mal à trouver des produits écoresponsables abordables.",
|
||
"Le processus d'achat en ligne est trop complexe.",
|
||
"Manque de transparence sur l’origine des produits.",
|
||
"Peu d’alternatives locales et durables sur le marché.",
|
||
],
|
||
2: [
|
||
"Jeunes urbains engagés dans la cause écologique.",
|
||
"Familles à revenu moyen voulant consommer responsable.",
|
||
"Entreprises soucieuses de leur empreinte carbone.",
|
||
],
|
||
3: [
|
||
"Une plateforme centralisée avec des produits écologiques certifiés.",
|
||
"Un service client humain et réactif.",
|
||
"Livraison éco-responsable avec suivi.",
|
||
],
|
||
4: [
|
||
"Application intuitive avec suggestions personnalisées.",
|
||
"Emballages recyclables et réutilisables.",
|
||
],
|
||
5: [
|
||
"Algorithme exclusif de recommandations durables.",
|
||
"Forte communauté engagée sur les réseaux.",
|
||
],
|
||
6: [
|
||
"Canaux digitaux : réseaux sociaux, SEO.",
|
||
"Partenariats avec influenceurs écoresponsables.",
|
||
"Boutique physique en pop-up stores.",
|
||
],
|
||
7: [
|
||
"Taux de rétention client mensuel.",
|
||
"Taux de satisfaction utilisateur (NPS).",
|
||
],
|
||
8: [
|
||
"Coût du développement logiciel initial.",
|
||
"Campagnes publicitaires et communication.",
|
||
"Frais logistiques (emballages, transport).",
|
||
],
|
||
9: [
|
||
"Ventes directes sur la plateforme.",
|
||
"Abonnement mensuel premium pour livraison gratuite.",
|
||
"Revenus via partenariats de marque.",
|
||
],
|
||
};
|
||
|
||
const section = leanCanvasData[title] || ["Aucune donnée disponible."];
|
||
|
||
const result = section.map(
|
||
(txt, index) =>
|
||
new SectionCell({
|
||
idSectionCell: index + 1,
|
||
sectionId: title,
|
||
contentSectionCell: txt,
|
||
modificationDate: date,
|
||
})
|
||
);
|
||
|
||
return new Promise<SectionCell[]>((resolve) => {
|
||
setTimeout(() => resolve(result), 500);
|
||
});
|
||
};
|
||
|
||
function getCurrentFormattedDate(): string {
|
||
const now = new Date();
|
||
const year = now.getFullYear();
|
||
const month = String(now.getMonth() + 1).padStart(2, "0"); // +1 car janvier = 0
|
||
const day = String(now.getDate()).padStart(2, "0");
|
||
const hours = String(now.getHours()).padStart(2, "0");
|
||
const minutes = String(now.getMinutes()).padStart(2, "0");
|
||
|
||
return `${year}-${month}-${day} ${hours}:${minutes}`;
|
||
}
|
||
|
||
onMounted(() => {
|
||
fetchData(
|
||
props.projectId,
|
||
props.title,
|
||
getCurrentFormattedDate(),
|
||
IS_MOCK_MODE
|
||
);
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
@import "@/components/canvas/style-project.css";
|
||
|
||
.cell {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
text-align: center;
|
||
transition: all 0.3s ease;
|
||
cursor: pointer;
|
||
box-shadow: 0 4px 5px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.expanded-content {
|
||
justify-content: flex-start !important;
|
||
}
|
||
|
||
.tooltip-explain {
|
||
position: absolute;
|
||
bottom: 101%;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
background-color: #333;
|
||
color: #fff;
|
||
padding: 6px 12px;
|
||
font-size: 13px;
|
||
border-radius: 6px;
|
||
white-space: nowrap;
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
transition: opacity 0.3s;
|
||
z-index: 10;
|
||
}
|
||
|
||
.cell:not(.expanded):hover .tooltip-explain {
|
||
opacity: 0.9;
|
||
}
|
||
|
||
.cell:not(.expanded):hover {
|
||
transform: scale(1.05);
|
||
box-shadow: 0 8px 9px rgba(0, 0, 0, 0.2);
|
||
}
|
||
|
||
.cell h3 {
|
||
font-size: 15px;
|
||
font-weight: 500;
|
||
font-family: "Arial", sans-serif;
|
||
}
|
||
|
||
.p {
|
||
font-size: 10px;
|
||
color: #666;
|
||
font-family: "Arial", sans-serif;
|
||
}
|
||
|
||
.expanded {
|
||
padding-top: 10%;
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: white;
|
||
z-index: 10;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
|
||
}
|
||
|
||
.description {
|
||
font-size: 5px;
|
||
color: #333;
|
||
word-break: break-word;
|
||
width: 90%;
|
||
margin: 5px 0;
|
||
}
|
||
|
||
.description + .p {
|
||
align-items: center;
|
||
justify-content: center;
|
||
text-align: center;
|
||
}
|
||
|
||
.edit-input {
|
||
width: 100%;
|
||
height: 100%;
|
||
min-height: 100px;
|
||
padding: 10px;
|
||
border: 1px solid #ccc;
|
||
border-radius: 5px;
|
||
margin-top: 10px;
|
||
box-sizing: border-box;
|
||
margin-left: 2%;
|
||
max-height: none;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.button-container {
|
||
display: block;
|
||
margin-top: 20px;
|
||
justify-content: center;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding-right: 1%;
|
||
}
|
||
|
||
.section-bloc {
|
||
background-color: #f3f3f3;
|
||
border-radius: 8px;
|
||
padding: 10px 12px;
|
||
font-family: "Arial", sans-serif;
|
||
color: #333;
|
||
word-break: break-word;
|
||
flex-shrink: 0;
|
||
cursor: default;
|
||
|
||
max-width: 100%;
|
||
width: fit-content;
|
||
overflow-wrap: break-word;
|
||
box-sizing: border-box;
|
||
|
||
min-width: 120px;
|
||
}
|
||
|
||
.editing-section-bloc {
|
||
width: 100%;
|
||
justify-content: center;
|
||
align-items: center;
|
||
display: flex;
|
||
margin-right: 10%;
|
||
margin: 10px;
|
||
}
|
||
|
||
.edit-button {
|
||
width: 100px;
|
||
height: 40px;
|
||
border: none;
|
||
border-radius: 5px;
|
||
cursor: pointer;
|
||
transition: background 0.3s ease;
|
||
font-size: 12px;
|
||
margin-right: 20px;
|
||
}
|
||
|
||
.description p {
|
||
font-size: 12px;
|
||
}
|
||
|
||
.save-button,
|
||
.cancel-button {
|
||
width: 100px;
|
||
height: 40px;
|
||
border: none;
|
||
border-radius: 5px;
|
||
cursor: pointer;
|
||
transition: background 0.3s ease;
|
||
font-size: 12px;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.edit-button {
|
||
background-color: #007bff;
|
||
color: white;
|
||
}
|
||
|
||
.save-button {
|
||
background-color: #28a745;
|
||
color: white;
|
||
}
|
||
|
||
.cancel-button {
|
||
background-color: #dc3545;
|
||
color: white;
|
||
}
|
||
|
||
.edit-button:hover {
|
||
background-color: #0056b3;
|
||
}
|
||
|
||
.save-button:hover {
|
||
background-color: #218838;
|
||
}
|
||
|
||
.cancel-button:hover {
|
||
background-color: #c82333;
|
||
}
|
||
|
||
.canvas-exit-hint {
|
||
font-size: 0.75rem;
|
||
color: #666;
|
||
position: fixed;
|
||
bottom: 10px;
|
||
left: 0;
|
||
width: 100%;
|
||
text-align: center;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.description-wrapper {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
text-align: center;
|
||
|
||
padding: 10px;
|
||
overflow: hidden;
|
||
max-height: 100%;
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
|
||
width: 100%;
|
||
overflow-x: hidden;
|
||
max-height: 100%;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.custom-flow {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
justify-content: flex-start;
|
||
align-items: flex-start;
|
||
padding: 10px;
|
||
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
overflow-x: hidden;
|
||
}
|
||
|
||
.container {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||
grid-auto-rows: min-content;
|
||
grid-auto-flow: dense;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.float-up {
|
||
transform: translateY(-10px);
|
||
}
|
||
.float-left {
|
||
transform: translateX(-10px);
|
||
}
|
||
.float-right {
|
||
transform: translateX(10px);
|
||
}
|
||
.wiggle {
|
||
transform: rotate(1deg);
|
||
}
|
||
.tilt {
|
||
transform: rotate(-1deg);
|
||
}
|
||
|
||
.from-left {
|
||
align-self: flex-start;
|
||
}
|
||
|
||
.from-right {
|
||
align-self: flex-end;
|
||
}
|
||
|
||
.section-bloc.from-left {
|
||
margin-right: auto;
|
||
margin-left: 20%;
|
||
}
|
||
|
||
.section-bloc.from-right {
|
||
margin-left: auto;
|
||
margin-right: 20%;
|
||
}
|
||
|
||
.explain {
|
||
font-size: 16px;
|
||
color: #444;
|
||
background-color: #f9f9f9;
|
||
padding: 7px;
|
||
border-radius: 6px;
|
||
margin-bottom: 20px;
|
||
margin-top: 20px;
|
||
line-height: 1.6;
|
||
font-family: "Segoe UI", sans-serif;
|
||
}
|
||
|
||
.delete-button {
|
||
position: absolute;
|
||
top: 5px;
|
||
right: 10px;
|
||
background: transparent;
|
||
border: none;
|
||
color: #a00;
|
||
font-size: 1.2rem;
|
||
cursor: pointer;
|
||
z-index: 10;
|
||
}
|
||
|
||
.section-bloc {
|
||
position: relative;
|
||
}
|
||
</style>
|