Ajout masquage de projets pour utilisateurs Pochet
All checks were successful
Deploy Preprod / Build and Deploy to Preprod (push) Successful in 28s

Backend :
- Blueprint pochet : champ hiddenProjects
- Route API toggle-hidden-project.php
- Controller site.php : retourne hiddenProjects + uuid dans projects

Frontend :
- Store user : hiddenProjects, visibleProjects, toggleHiddenProject()
- Store projects : filtrage automatique des projets masqués
- Store api : fonction toggleHiddenProject()
- Account.vue : section projets avec cartes horizontales + boutons toggle
  - Affichage pour Pochet (avec toggle) et Client (sans toggle)
  - Section client masquée pour Pochet

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
isUnknown 2026-01-30 11:27:27 +01:00
parent 7ca72c6d82
commit 30b7697c64
8 changed files with 263 additions and 5 deletions

View file

@ -19,3 +19,8 @@ fields:
type: pages
query: page('projects').children
width: 3/4
hiddenProjects:
label: Projets masqués
type: pages
query: page('projects').children
width: 3/4

View file

@ -31,6 +31,7 @@ return [
require(__DIR__ . '/routes/logout.php'),
require(__DIR__ . '/routes/login.php'),
require(__DIR__ . '/routes/toggle-favorite.php'),
require(__DIR__ . '/routes/toggle-hidden-project.php'),
require(__DIR__ . '/routes/upload-images.php'),
require(__DIR__ . '/routes/save-page.php'),
require(__DIR__ . '/routes/save-file.php'),

View file

@ -0,0 +1,61 @@
<?php
return [
'pattern' => '/toggle-hidden-project.json',
'method' => 'POST',
'action' => function() {
$json = file_get_contents("php://input");
$data = json_decode($json);
try {
$user = kirby()->user();
$projectUuid = $data->projectUuid;
// Récupérer les projets masqués actuels
$hiddenProjects = $user->hiddenProjects()->toPages();
$hiddenProjectsArray = [];
foreach ($hiddenProjects as $project) {
$hiddenProjectsArray[] = 'page://' . $project->uuid();
}
// Toggle: ajouter ou retirer le projet
$projectPage = Find::page($projectUuid);
$projectRef = 'page://' . $projectPage->uuid();
$index = array_search($projectRef, $hiddenProjectsArray);
if ($index !== false) {
// Projet déjà masqué, on le retire
unset($hiddenProjectsArray[$index]);
$action = 'shown';
} else {
// Projet non masqué, on l'ajoute
$hiddenProjectsArray[] = $projectRef;
$action = 'hidden';
}
// Réindexer le tableau
$hiddenProjectsArray = array_values($hiddenProjectsArray);
// Encoder en YAML pour Kirby
$yaml = Data::encode($hiddenProjectsArray, 'yaml');
// Mettre à jour l'utilisateur
$user->update([
'hiddenProjects' => $yaml
]);
return [
'status' => 'success',
'action' => $action,
'hiddenProjects' => $hiddenProjectsArray
];
} catch (\Throwable $th) {
return [
'status' => 'error',
'message' => 'Impossible de modifier les projets masqués : ' . $th->getMessage() . ' in file ' . $th->getFile() . ' line ' . $th->getLine()
];
}
}
];

View file

@ -33,9 +33,18 @@ return function ($page, $kirby, $site) {
"title" => (string) $project->title(),
"uri" => (string) $project->uri(),
"step" => (string) $project->getStepLabel(),
"uuid" => (string) $project->uuid(),
];
})->data;
}
if ($kirby->user()->hiddenProjects()->exists() && $kirby->user()->hiddenProjects()->isNotEmpty()) {
$userData['hiddenProjects'] = $kirby->user()->hiddenProjects()->toPages()->map(function ($project) {
return (string) $project->uuid();
})->data;
} else {
$userData['hiddenProjects'] = [];
}
}

View file

@ -289,6 +289,32 @@ export const useApiStore = defineStore("api", () => {
}
}
async function toggleHiddenProject(projectUuid) {
const headers = {
method: "POST",
body: JSON.stringify({ projectUuid }),
};
try {
const response = await fetch("/toggle-hidden-project.json", headers);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.status === "success") {
userStore.toggleHiddenProject(projectUuid);
console.log("Projet masqué/affiché avec succès.");
return data;
} else {
throw new Error(data.message);
}
} catch (error) {
console.error("Erreur lors du toggle du projet masqué:", error);
throw error;
}
}
return {
fetchData,
fetchRoute,
@ -299,6 +325,7 @@ export const useApiStore = defineStore("api", () => {
// Nouvelles fonctions
markNotificationRead,
markAllNotificationsRead,
toggleHiddenProject,
// Anciennes fonctions (rétro-compatibilité)
readNotification,
readAllNotifications,

View file

@ -1,5 +1,6 @@
import { defineStore } from 'pinia';
import { useApiStore } from './api.js';
import { useUserStore } from './user.js';
import { ref, computed } from 'vue';
export const useProjectsStore = defineStore('projects', () => {
@ -7,25 +8,40 @@ export const useProjectsStore = defineStore('projects', () => {
const projects = ref(null);
const currentProjects = computed(() => {
const userStore = useUserStore();
const hiddenProjectUuids = userStore.hiddenProjects || [];
return (
projects.value
?.filter((project) => project.status === 'listed')
?.filter((project) =>
project.status === 'listed' &&
!hiddenProjectUuids.includes(project.uuid)
)
.sort((a, b) => new Date(b.modified) - new Date(a.modified)) ?? []
);
});
const draftProjects = computed(() => {
const userStore = useUserStore();
const hiddenProjectUuids = userStore.hiddenProjects || [];
return (
projects.value
?.filter((project) => project.status === 'draft')
?.filter((project) =>
project.status === 'draft' &&
!hiddenProjectUuids.includes(project.uuid)
)
.sort((a, b) => new Date(b.modified) - new Date(a.modified)) ?? []
);
});
const archivedProjects = computed(() => {
const userStore = useUserStore();
const hiddenProjectUuids = userStore.hiddenProjects || [];
return (
projects.value
?.filter((project) => project.status === 'unlisted')
?.filter((project) =>
project.status === 'unlisted' &&
!hiddenProjectUuids.includes(project.uuid)
)
.sort((a, b) => new Date(b.modified) - new Date(a.modified)) ?? []
);
});

View file

@ -87,13 +87,40 @@ export const useUserStore = defineStore('user', () => {
return user.value.uuid === comment.author.uuid;
}
const hiddenProjects = computed(() => {
return user.value?.hiddenProjects || [];
});
const visibleProjects = computed(() => {
if (!user.value?.projects) return [];
const projectsArray = Array.isArray(user.value.projects)
? user.value.projects
: Object.values(user.value.projects);
return projectsArray.filter(
(project) => !hiddenProjects.value.includes(project.uuid)
);
});
function toggleHiddenProject(projectUuid) {
if (!user.value) return;
const index = user.value.hiddenProjects.indexOf(projectUuid);
if (index > -1) {
user.value.hiddenProjects.splice(index, 1);
} else {
user.value.hiddenProjects.push(projectUuid);
}
}
return {
user,
isLogged,
notifications,
hiddenProjects,
visibleProjects,
// Nouvelles fonctions
markNotificationRead,
markAllNotificationsRead,
toggleHiddenProject,
// Anciennes fonctions (rétro-compatibilité)
readNotification,
readAllNotifications,

View file

@ -198,6 +198,7 @@
</section>
<section
v-if="user.role !== 'pochet'"
class="bg-white rounded-2xl px-16 py-24"
aria-labelledby="client-label"
>
@ -219,7 +220,90 @@
</section>
<section
v-if="user.hasOwnProperty('projects')"
v-if="
user.hasOwnProperty('projects') &&
(user.role === 'pochet' || user.role === 'client')
"
class="bg-white rounded-2xl px-16 py-24"
aria-labelledby="projects-label"
>
<h2 id="projects-label" class="text-grey-700 mb-24">
{{ user.role === 'pochet' ? 'Projets managés' : 'Mes projets' }}
</h2>
<div class="projects-list flow" style="--flow-space: 1rem">
<div
v-for="project in user.role === 'pochet'
? visibleProjects
: allProjectsList"
:key="project.uuid"
class="project-card | flex items-center | bg-grey-100 rounded-lg px-16 py-12"
style="--column-gap: 1rem"
>
<button
v-if="user.role === 'pochet'"
@click="toggleProject(project.uuid)"
class="btn btn--sm btn--primary"
:title="'Masquer le projet'"
aria-label="Masquer le projet"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="16"
height="16"
fill="currentColor"
>
<path
d="M12.0003 3C17.3924 3 21.8784 6.87976 22.8189 12C21.8784 17.1202 17.3924 21 12.0003 21C6.60812 21 2.12215 17.1202 1.18164 12C2.12215 6.87976 6.60812 3 12.0003 3ZM12.0003 19C16.2359 19 19.8603 16.052 20.7777 12C19.8603 7.94803 16.2359 5 12.0003 5C7.7646 5 4.14022 7.94803 3.22278 12C4.14022 16.052 7.7646 19 12.0003 19ZM12.0003 16.5C9.51498 16.5 7.50026 14.4853 7.50026 12C7.50026 9.51472 9.51498 7.5 12.0003 7.5C14.4855 7.5 16.5003 9.51472 16.5003 12C16.5003 14.4853 14.4855 16.5 12.0003 16.5ZM12.0003 14.5C13.381 14.5 14.5003 13.3807 14.5003 12C14.5003 10.6193 13.381 9.5 12.0003 9.5C10.6196 9.5 9.50026 10.6193 9.50026 12C9.50026 13.3807 10.6196 14.5 12.0003 14.5Z"
></path>
</svg>
</button>
<div class="flex-1 | flex flex-col">
<p class="font-medium">{{ project.title }}</p>
<p class="text-sm text-grey-600">{{ project.step }}</p>
</div>
</div>
<template
v-if="user.role === 'pochet' && hiddenProjectsList.length > 0"
>
<h3 class="text-grey-700 mt-24 mb-12">Projets masqués</h3>
<div
v-for="project in hiddenProjectsList"
:key="project.uuid"
class="project-card | flex items-center | bg-grey-100 rounded-lg px-16 py-12"
style="--column-gap: 1rem; opacity: 0.5"
>
<button
@click="toggleProject(project.uuid)"
class="btn btn--sm btn--primary"
:title="'Afficher le projet'"
aria-label="Afficher le projet"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="16"
height="16"
fill="currentColor"
>
<path
d="M17.8827 19.2968C16.1814 20.3755 14.1638 21.0002 12.0003 21.0002C6.60812 21.0002 2.12215 17.1204 1.18164 12.0002C1.61832 9.62282 2.81932 7.5129 4.52047 5.93457L1.39366 2.80777L2.80788 1.39355L22.6069 21.1925L21.1927 22.6068L17.8827 19.2968ZM5.9356 7.3497C4.60673 8.56015 3.6378 10.1672 3.22278 12.0002C4.14022 16.0521 7.7646 19.0002 12.0003 19.0002C13.5997 19.0002 15.112 18.5798 16.4243 17.8384L14.396 15.8101C13.7023 16.2472 12.8808 16.5002 12.0003 16.5002C9.51498 16.5002 7.50026 14.4854 7.50026 12.0002C7.50026 11.1196 7.75317 10.2981 8.19031 9.60442L5.9356 7.3497ZM12.9139 14.328L9.67246 11.0866C9.5613 11.3696 9.50026 11.6777 9.50026 12.0002C9.50026 13.3809 10.6196 14.5002 12.0003 14.5002C12.3227 14.5002 12.6309 14.4391 12.9139 14.328ZM20.8068 16.5925L19.376 15.1617C20.0319 14.2268 20.5154 13.1586 20.7777 12.0002C19.8603 7.94818 16.2359 5.00016 12.0003 5.00016C11.1544 5.00016 10.3329 5.11773 9.55249 5.33818L7.97446 3.76015C9.22127 3.26959 10.5793 3.00016 12.0003 3.00016C17.3924 3.00016 21.8784 6.87992 22.8189 12.0002C22.5067 13.6998 21.8038 15.2628 20.8068 16.5925ZM11.7229 7.50857C11.8146 7.50299 11.9071 7.50016 12.0003 7.50016C14.4855 7.50016 16.5003 9.51488 16.5003 12.0002C16.5003 12.0933 16.4974 12.1858 16.4919 12.2775L11.7229 7.50857Z"
></path>
</svg>
</button>
<div class="flex-1 | flex flex-col">
<p class="font-medium">{{ project.title }}</p>
<p class="text-sm text-grey-600">{{ project.step }}</p>
</div>
</div>
</template>
</div>
</section>
<section
v-else-if="user.hasOwnProperty('projects')"
class="bg-white rounded-2xl px-16 py-24"
aria-labelledby="projects-label"
>
@ -233,9 +317,37 @@
<script setup>
import { storeToRefs } from 'pinia';
import { useUserStore } from '../stores/user';
import { useApiStore } from '../stores/api';
import { computed, ref, watch } from 'vue';
const { user } = storeToRefs(useUserStore());
const userStore = useUserStore();
const { user, visibleProjects } = storeToRefs(userStore);
const api = useApiStore();
const hiddenProjectsList = computed(() => {
if (!user.value?.projects || !user.value?.hiddenProjects) return [];
const projectsArray = Array.isArray(user.value.projects)
? user.value.projects
: Object.values(user.value.projects);
return projectsArray.filter((project) =>
user.value.hiddenProjects.includes(project.uuid)
);
});
const allProjectsList = computed(() => {
if (!user.value?.projects) return [];
return Array.isArray(user.value.projects)
? user.value.projects
: Object.values(user.value.projects);
});
async function toggleProject(projectUuid) {
try {
await api.toggleHiddenProject(projectUuid);
} catch (error) {
console.error('Erreur lors du masquage/affichage du projet:', error);
}
}
// Email
const email = ref('');