Refonte du système de notifications : passage aux notifications dérivées
Remplace le système de notifications stockées par un système de providers qui dérivent les notifications des données existantes (commentaires, réponses, demandes de projet, demandes de rendez-vous, validations de brief). - Ajout du NotificationCollector et de l'interface NotificationProvider - Création de 5 providers : Comment, Reply, ProjectRequest, AppointmentRequest, Content - Métadonnées de notifications stockées directement sur les entités source - Nouvelles routes mark-as-read et mark-all-read - Mise à jour du frontend pour le nouveau système - Route de migration pour les données existantes Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
c68b51f639
commit
a7d315942a
26 changed files with 1406 additions and 137 deletions
|
|
@ -46,6 +46,7 @@ return [
|
|||
'author' => kirby()->user(),
|
||||
'id' => Str::uuid(),
|
||||
'type' => 'comment',
|
||||
'readby' => [], // Pour le système de notifications dérivées
|
||||
];
|
||||
|
||||
if (isset($data->position->pageIndex)) {
|
||||
|
|
@ -62,11 +63,8 @@ return [
|
|||
|
||||
echo json_encode(getFileData($newFile));
|
||||
|
||||
try {
|
||||
$project->createNotification($commentData);
|
||||
} catch (\Throwable $th) {
|
||||
throw new Exception($th->getMessage() . '. line ' . $th->getLine() . ' in file ' . $th->getFile(), 1);
|
||||
}
|
||||
// Note: Les notifications sont maintenant dérivées des commentaires.
|
||||
// Plus besoin d'appeler createNotification().
|
||||
|
||||
exit;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -39,8 +39,8 @@ return [
|
|||
|
||||
echo json_encode(getFileData($newFile));
|
||||
|
||||
$project = $page->parents()->findBy('template', 'project');
|
||||
$project->deleteNotification($data->id);
|
||||
// Note: Les notifications sont maintenant dérivées des commentaires.
|
||||
// La suppression du commentaire supprime automatiquement la notification.
|
||||
|
||||
exit;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -31,18 +31,19 @@ return [
|
|||
"author" => kirby()->user(),
|
||||
"id" => Str::uuid(),
|
||||
"type" => "comment-reply",
|
||||
"readby" => [], // Pour le système de notifications dérivées
|
||||
];
|
||||
$newReply = new Reply($replyData);
|
||||
$comment['replies'][] = $newReply->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$newFile = $file->update([
|
||||
'comments' => $comments
|
||||
]);
|
||||
|
||||
$project = $page->parents()->findBy("template", "project");
|
||||
$project->createNotification($replyData);
|
||||
// Note: Les notifications sont maintenant dérivées des commentaires.
|
||||
// Plus besoin d'appeler createNotification().
|
||||
|
||||
return getFileData($newFile);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,54 @@
|
|||
<?php
|
||||
|
||||
load([
|
||||
"ProjectPage" => "models/ProjectPage.php",
|
||||
], __DIR__);
|
||||
use adrienpayet\notifications\NotificationCollector;
|
||||
use adrienpayet\notifications\providers\CommentProvider;
|
||||
use adrienpayet\notifications\providers\ReplyProvider;
|
||||
use adrienpayet\notifications\providers\ProjectRequestProvider;
|
||||
use adrienpayet\notifications\providers\AppointmentRequestProvider;
|
||||
use adrienpayet\notifications\providers\ContentProvider;
|
||||
|
||||
// Charger les classes
|
||||
F::loadClasses([
|
||||
// Own classes
|
||||
"adrienpayet\\notifications\\Notification" => __DIR__ . "/src/Notification.php",
|
||||
"adrienpayet\\notifications\\NotificationsPage" => __DIR__ . "/src/NotificationsPage.php",
|
||||
|
||||
// Shared classes
|
||||
"adrienpayet\\D2P\\data\\Position" => __DIR__ . "/../classes/Position.php",
|
||||
"adrienpayet\\D2P\data\Author" => __DIR__ . "/../classes/Author.php",
|
||||
"adrienpayet\\D2P\\data\\location\\Location" => __DIR__ . "/../classes/location/Location.php",
|
||||
"adrienpayet\\D2P\\data\\location\\PageDetails" => __DIR__ . "/../classes/location/PageDetails.php",
|
||||
"adrienpayet\\D2P\\data\\location\\ProjectDetails" => __DIR__ . "/../classes/location/ProjectDetails.php",
|
||||
"adrienpayet\\D2P\\data\\location\\FileDetails" => __DIR__ . "/../classes/location/FileDetails.php",
|
||||
// Nouvelles classes - Système de providers
|
||||
"adrienpayet\\notifications\\NotificationProvider" => __DIR__ . "/src/NotificationProvider.php",
|
||||
"adrienpayet\\notifications\\NotificationCollector" => __DIR__ . "/src/NotificationCollector.php",
|
||||
"adrienpayet\\notifications\\providers\\CommentProvider" => __DIR__ . "/src/providers/CommentProvider.php",
|
||||
"adrienpayet\\notifications\\providers\\ReplyProvider" => __DIR__ . "/src/providers/ReplyProvider.php",
|
||||
"adrienpayet\\notifications\\providers\\ProjectRequestProvider" => __DIR__ . "/src/providers/ProjectRequestProvider.php",
|
||||
"adrienpayet\\notifications\\providers\\AppointmentRequestProvider" => __DIR__ . "/src/providers/AppointmentRequestProvider.php",
|
||||
"adrienpayet\\notifications\\providers\\ContentProvider" => __DIR__ . "/src/providers/ContentProvider.php",
|
||||
|
||||
// Anciennes classes - Gardées pour rétro-compatibilité pendant migration
|
||||
"adrienpayet\\notifications\\Notification" => __DIR__ . "/src/Notification.php",
|
||||
"adrienpayet\\notifications\\NotificationsPage" => __DIR__ . "/src/NotificationsPage.php",
|
||||
|
||||
// Classes partagées
|
||||
"adrienpayet\\D2P\\data\\Position" => __DIR__ . "/../classes/Position.php",
|
||||
"adrienpayet\\D2P\\data\\Author" => __DIR__ . "/../classes/Author.php",
|
||||
"adrienpayet\\D2P\\data\\location\\Location" => __DIR__ . "/../classes/location/Location.php",
|
||||
"adrienpayet\\D2P\\data\\location\\PageDetails" => __DIR__ . "/../classes/location/PageDetails.php",
|
||||
"adrienpayet\\D2P\\data\\location\\ProjectDetails" => __DIR__ . "/../classes/location/ProjectDetails.php",
|
||||
"adrienpayet\\D2P\\data\\location\\FileDetails" => __DIR__ . "/../classes/location/FileDetails.php",
|
||||
]);
|
||||
|
||||
// Créer et configurer le collector
|
||||
$collector = new NotificationCollector();
|
||||
$collector->register(new CommentProvider());
|
||||
$collector->register(new ReplyProvider());
|
||||
$collector->register(new ProjectRequestProvider());
|
||||
$collector->register(new AppointmentRequestProvider());
|
||||
$collector->register(new ContentProvider());
|
||||
|
||||
Kirby::plugin("adrienpayet/pdc-notifications", [
|
||||
"routes" => [
|
||||
require(__DIR__ . "/routes/readAll.php"),
|
||||
require(__DIR__ . "/routes/read.php")
|
||||
],
|
||||
"options" => [
|
||||
"collector" => $collector
|
||||
],
|
||||
"routes" => [
|
||||
// Nouvelles routes
|
||||
require(__DIR__ . "/routes/mark-as-read.php"),
|
||||
require(__DIR__ . "/routes/mark-all-read.php"),
|
||||
// Anciennes routes - Gardées pour rétro-compatibilité
|
||||
require(__DIR__ . "/routes/readAll.php"),
|
||||
require(__DIR__ . "/routes/read.php"),
|
||||
],
|
||||
]);
|
||||
|
|
|
|||
42
public/site/plugins/notifications/routes/mark-all-read.php
Normal file
42
public/site/plugins/notifications/routes/mark-all-read.php
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Route pour marquer toutes les notifications comme lues.
|
||||
* Parcourt tous les projets accessibles à l'utilisateur.
|
||||
*/
|
||||
return [
|
||||
'pattern' => '(:all)mark-all-notifications-read.json',
|
||||
'method' => 'POST',
|
||||
'action' => function () {
|
||||
try {
|
||||
$user = kirby()->user();
|
||||
if (!$user) {
|
||||
throw new Exception('User not authenticated');
|
||||
}
|
||||
|
||||
$collector = kirby()->option('adrienpayet.pdc-notifications.collector');
|
||||
if (!$collector) {
|
||||
throw new Exception('NotificationCollector not initialized');
|
||||
}
|
||||
|
||||
// Récupérer les projets selon le rôle
|
||||
if ($user->role()->name() === 'admin') {
|
||||
$projects = page('projects')->children()->toArray();
|
||||
} else {
|
||||
$projects = $user->projects()->toPages()->toArray();
|
||||
}
|
||||
|
||||
$count = $collector->markAllAsRead($projects, $user);
|
||||
|
||||
return json_encode([
|
||||
'status' => 'success',
|
||||
'message' => "$count notifications marked as read"
|
||||
]);
|
||||
} catch (\Throwable $th) {
|
||||
return json_encode([
|
||||
'status' => 'error',
|
||||
'message' => $th->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
];
|
||||
46
public/site/plugins/notifications/routes/mark-as-read.php
Normal file
46
public/site/plugins/notifications/routes/mark-as-read.php
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Route pour marquer une notification comme lue.
|
||||
* Délègue au bon provider selon le type de notification.
|
||||
*/
|
||||
return [
|
||||
'pattern' => '(:all)mark-notification-read.json',
|
||||
'method' => 'POST',
|
||||
'action' => function () {
|
||||
$json = file_get_contents('php://input');
|
||||
$data = json_decode($json);
|
||||
|
||||
if (!$data || !isset($data->type) || !isset($data->id)) {
|
||||
return json_encode([
|
||||
'status' => 'error',
|
||||
'message' => 'Missing required fields: type, id'
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
$collector = kirby()->option('adrienpayet.pdc-notifications.collector');
|
||||
|
||||
if (!$collector) {
|
||||
throw new Exception('NotificationCollector not initialized');
|
||||
}
|
||||
|
||||
$success = $collector->markAsRead(
|
||||
$data->type,
|
||||
$data->id,
|
||||
(array) $data,
|
||||
kirby()->user()
|
||||
);
|
||||
|
||||
return json_encode([
|
||||
'status' => $success ? 'success' : 'error',
|
||||
'message' => $success ? 'Notification marked as read' : 'Failed to mark notification as read'
|
||||
]);
|
||||
} catch (\Throwable $th) {
|
||||
return json_encode([
|
||||
'status' => 'error',
|
||||
'message' => $th->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
];
|
||||
123
public/site/plugins/notifications/src/NotificationCollector.php
Normal file
123
public/site/plugins/notifications/src/NotificationCollector.php
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
<?php
|
||||
|
||||
namespace adrienpayet\notifications;
|
||||
|
||||
use Kirby\Cms\Page;
|
||||
use Kirby\Cms\User;
|
||||
|
||||
/**
|
||||
* Collecteur de notifications qui agrège tous les providers.
|
||||
*
|
||||
* Permet de :
|
||||
* - Enregistrer des providers de notifications
|
||||
* - Collecter toutes les notifications de tous les providers
|
||||
* - Déléguer le markAsRead au bon provider
|
||||
*/
|
||||
class NotificationCollector
|
||||
{
|
||||
/** @var NotificationProvider[] */
|
||||
private array $providers = [];
|
||||
|
||||
/**
|
||||
* Enregistre un nouveau provider.
|
||||
*/
|
||||
public function register(NotificationProvider $provider): void
|
||||
{
|
||||
$this->providers[$provider->getType()] = $provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collecte toutes les notifications de tous les providers pour un projet.
|
||||
*
|
||||
* @param Page $project Le projet à scanner
|
||||
* @param User $user L'utilisateur courant
|
||||
* @return array Liste triée par date décroissante
|
||||
*/
|
||||
public function collect(Page $project, User $user): array
|
||||
{
|
||||
$all = [];
|
||||
|
||||
foreach ($this->providers as $provider) {
|
||||
try {
|
||||
$notifications = $provider->collect($project, $user);
|
||||
$all = array_merge($all, $notifications);
|
||||
} catch (\Throwable $e) {
|
||||
// Log l'erreur mais continue avec les autres providers
|
||||
error_log("NotificationCollector: Error in {$provider->getType()}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Trier par date décroissante
|
||||
usort($all, function ($a, $b) {
|
||||
$dateA = strtotime($a['date'] ?? '0');
|
||||
$dateB = strtotime($b['date'] ?? '0');
|
||||
return $dateB - $dateA;
|
||||
});
|
||||
|
||||
return $all;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marque une notification comme lue en déléguant au bon provider.
|
||||
*
|
||||
* @param string $type Le type de notification
|
||||
* @param string $id L'identifiant de la notification
|
||||
* @param array $location Informations de localisation
|
||||
* @param User $user L'utilisateur qui marque comme lu
|
||||
* @return bool True si succès
|
||||
*/
|
||||
public function markAsRead(string $type, string $id, array $location, User $user): bool
|
||||
{
|
||||
if (!isset($this->providers[$type])) {
|
||||
error_log("NotificationCollector: Unknown notification type: $type");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->providers[$type]->markAsRead($id, $location, $user);
|
||||
} catch (\Throwable $e) {
|
||||
error_log("NotificationCollector: Error marking as read: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marque toutes les notifications comme lues pour un utilisateur.
|
||||
*
|
||||
* @param Page[] $projects Liste des projets
|
||||
* @param User $user L'utilisateur
|
||||
* @return int Nombre de notifications marquées comme lues
|
||||
*/
|
||||
public function markAllAsRead(array $projects, User $user): int
|
||||
{
|
||||
$count = 0;
|
||||
|
||||
foreach ($projects as $project) {
|
||||
$notifications = $this->collect($project, $user);
|
||||
|
||||
foreach ($notifications as $notification) {
|
||||
if (!($notification['isRead'] ?? false)) {
|
||||
$success = $this->markAsRead(
|
||||
$notification['type'],
|
||||
$notification['id'],
|
||||
$notification,
|
||||
$user
|
||||
);
|
||||
if ($success) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne la liste des types de notifications enregistrés.
|
||||
*/
|
||||
public function getRegisteredTypes(): array
|
||||
{
|
||||
return array_keys($this->providers);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
namespace adrienpayet\notifications;
|
||||
|
||||
use Kirby\Cms\Page;
|
||||
use Kirby\Cms\User;
|
||||
|
||||
/**
|
||||
* Interface pour les providers de notifications.
|
||||
*
|
||||
* Chaque type de notification (comment, project-request, etc.)
|
||||
* a son propre provider qui sait :
|
||||
* - Collecter les notifications depuis la source de données
|
||||
* - Marquer une notification comme lue sur la source
|
||||
*/
|
||||
interface NotificationProvider
|
||||
{
|
||||
/**
|
||||
* Retourne le type de notification géré par ce provider.
|
||||
* Ex: 'comment', 'comment-reply', 'project-request'
|
||||
*/
|
||||
public function getType(): string;
|
||||
|
||||
/**
|
||||
* Collecte toutes les notifications de ce type pour un projet et un utilisateur.
|
||||
*
|
||||
* @param Page $project Le projet à scanner
|
||||
* @param User $user L'utilisateur courant (pour filtrer ses propres actions)
|
||||
* @return array Liste des notifications au format standard
|
||||
*/
|
||||
public function collect(Page $project, User $user): array;
|
||||
|
||||
/**
|
||||
* Marque une notification comme lue.
|
||||
*
|
||||
* @param string $id L'identifiant de la notification
|
||||
* @param array $location Informations de localisation (ex: _file, _projectUri)
|
||||
* @param User $user L'utilisateur qui marque comme lu
|
||||
* @return bool True si succès, false sinon
|
||||
*/
|
||||
public function markAsRead(string $id, array $location, User $user): bool;
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
<?php
|
||||
|
||||
namespace adrienpayet\notifications\providers;
|
||||
|
||||
use adrienpayet\notifications\NotificationProvider;
|
||||
use Kirby\Cms\Page;
|
||||
use Kirby\Cms\User;
|
||||
use Kirby\Data\Yaml;
|
||||
|
||||
/**
|
||||
* Provider pour les notifications de type "appointment-request".
|
||||
* Dérivé depuis les champs du projet quand hasOptimizationRequest est true.
|
||||
*/
|
||||
class AppointmentRequestProvider implements NotificationProvider
|
||||
{
|
||||
public function getType(): string
|
||||
{
|
||||
return 'appointment-request';
|
||||
}
|
||||
|
||||
public function collect(Page $project, User $user): array
|
||||
{
|
||||
// Pas de notification si pas de demande d'optimisation
|
||||
if ($project->hasOptimizationRequest()->isFalse()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Vérifier que les champs requis existent
|
||||
if ($project->optimizationAuthor()->isEmpty()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$userUuid = (string) $user->uuid();
|
||||
$authorUuid = $project->optimizationAuthor()->value();
|
||||
|
||||
// Ne pas notifier l'auteur de sa propre demande
|
||||
if ($authorUuid === $userUuid) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$readby = $project->optimizationReadby()->isNotEmpty()
|
||||
? Yaml::decode($project->optimizationReadby()->value())
|
||||
: [];
|
||||
|
||||
if (!is_array($readby)) {
|
||||
$readby = [];
|
||||
}
|
||||
|
||||
return [[
|
||||
'id' => 'appointment-request-' . (string) $project->uuid(),
|
||||
'type' => 'appointment-request',
|
||||
'text' => $project->optimizationRequestDetails()->value() ?? '',
|
||||
'author' => [
|
||||
'uuid' => $authorUuid,
|
||||
'name' => $project->optimizationAuthorName()->value() ?? '',
|
||||
'email' => $project->optimizationAuthorEmail()->value() ?? '',
|
||||
'role' => 'client',
|
||||
],
|
||||
'date' => $project->optimizationDate()->value() ?? '',
|
||||
'location' => [
|
||||
'page' => [
|
||||
'uri' => $project->uri(),
|
||||
'title' => (string) $project->title(),
|
||||
'template' => 'project',
|
||||
],
|
||||
'project' => [
|
||||
'uri' => $project->uri(),
|
||||
'title' => (string) $project->title(),
|
||||
]
|
||||
],
|
||||
'readby' => $readby,
|
||||
'isRead' => in_array($userUuid, $readby),
|
||||
'_projectUri' => $project->uri(),
|
||||
]];
|
||||
}
|
||||
|
||||
public function markAsRead(string $id, array $location, User $user): bool
|
||||
{
|
||||
$projectUri = $location['_projectUri'] ?? null;
|
||||
if (!$projectUri) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$project = page($projectUri);
|
||||
if (!$project) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$readby = $project->optimizationReadby()->isNotEmpty()
|
||||
? Yaml::decode($project->optimizationReadby()->value())
|
||||
: [];
|
||||
|
||||
if (!is_array($readby)) {
|
||||
$readby = [];
|
||||
}
|
||||
|
||||
$userUuid = (string) $user->uuid();
|
||||
|
||||
if (in_array($userUuid, $readby)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$readby[] = $userUuid;
|
||||
|
||||
$project->update([
|
||||
'optimizationReadby' => array_unique($readby)
|
||||
]);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
<?php
|
||||
|
||||
namespace adrienpayet\notifications\providers;
|
||||
|
||||
use adrienpayet\notifications\NotificationProvider;
|
||||
use Kirby\Cms\Page;
|
||||
use Kirby\Cms\User;
|
||||
use Kirby\Data\Yaml;
|
||||
|
||||
/**
|
||||
* Provider pour les notifications de type "comment".
|
||||
* Collecte les commentaires depuis les fichiers des étapes du projet.
|
||||
*/
|
||||
class CommentProvider implements NotificationProvider
|
||||
{
|
||||
public function getType(): string
|
||||
{
|
||||
return 'comment';
|
||||
}
|
||||
|
||||
public function collect(Page $project, User $user): array
|
||||
{
|
||||
$notifications = [];
|
||||
$userUuid = (string) $user->uuid();
|
||||
|
||||
// Parcourir toutes les étapes du projet
|
||||
foreach ($project->children() as $step) {
|
||||
// Parcourir tous les fichiers de chaque étape
|
||||
foreach ($step->files() as $file) {
|
||||
if ($file->comments()->isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$comments = Yaml::decode($file->comments()->value());
|
||||
if (!is_array($comments)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($comments as $comment) {
|
||||
// Ignorer les commentaires de type reply (gérés par ReplyProvider)
|
||||
if (($comment['type'] ?? 'comment') === 'comment-reply') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ne pas notifier l'auteur de son propre commentaire
|
||||
$authorUuid = $comment['author']['uuid'] ?? '';
|
||||
if ($authorUuid === $userUuid) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$readby = $comment['readby'] ?? [];
|
||||
|
||||
$location = $comment['location'] ?? [];
|
||||
// Assurer que location.project existe toujours
|
||||
if (!isset($location['project'])) {
|
||||
$location['project'] = [
|
||||
'uri' => $project->uri(),
|
||||
'title' => (string) $project->title(),
|
||||
];
|
||||
}
|
||||
|
||||
$notifications[] = [
|
||||
'id' => $comment['id'],
|
||||
'type' => 'comment',
|
||||
'text' => $comment['text'] ?? '',
|
||||
'author' => $comment['author'] ?? [],
|
||||
'date' => $comment['date'] ?? '',
|
||||
'location' => $location,
|
||||
'position' => $comment['position'] ?? [],
|
||||
'readby' => $readby,
|
||||
'isRead' => in_array($userUuid, $readby),
|
||||
// Métadonnées pour markAsRead
|
||||
'_file' => (string) $file->uuid(),
|
||||
'_stepUri' => $step->uri(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Parcourir aussi les sous-pages (ex: tracks dans virtual-sample)
|
||||
foreach ($step->children() as $subPage) {
|
||||
foreach ($subPage->files() as $file) {
|
||||
if ($file->comments()->isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$comments = Yaml::decode($file->comments()->value());
|
||||
if (!is_array($comments)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($comments as $comment) {
|
||||
if (($comment['type'] ?? 'comment') === 'comment-reply') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$authorUuid = $comment['author']['uuid'] ?? '';
|
||||
if ($authorUuid === $userUuid) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$readby = $comment['readby'] ?? [];
|
||||
|
||||
$location = $comment['location'] ?? [];
|
||||
// Assurer que location.project existe toujours
|
||||
if (!isset($location['project'])) {
|
||||
$location['project'] = [
|
||||
'uri' => $project->uri(),
|
||||
'title' => (string) $project->title(),
|
||||
];
|
||||
}
|
||||
|
||||
$notifications[] = [
|
||||
'id' => $comment['id'],
|
||||
'type' => 'comment',
|
||||
'text' => $comment['text'] ?? '',
|
||||
'author' => $comment['author'] ?? [],
|
||||
'date' => $comment['date'] ?? '',
|
||||
'location' => $location,
|
||||
'position' => $comment['position'] ?? [],
|
||||
'readby' => $readby,
|
||||
'isRead' => in_array($userUuid, $readby),
|
||||
'_file' => (string) $file->uuid(),
|
||||
'_stepUri' => $subPage->uri(),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $notifications;
|
||||
}
|
||||
|
||||
public function markAsRead(string $id, array $location, User $user): bool
|
||||
{
|
||||
$fileUuid = $location['_file'] ?? null;
|
||||
if (!$fileUuid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Trouver le fichier par UUID (peut être avec ou sans préfixe file://)
|
||||
$fileUri = str_starts_with($fileUuid, 'file://') ? $fileUuid : 'file://' . $fileUuid;
|
||||
$file = kirby()->file($fileUri);
|
||||
if (!$file) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$comments = Yaml::decode($file->comments()->value());
|
||||
if (!is_array($comments)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$userUuid = (string) $user->uuid();
|
||||
$updated = false;
|
||||
|
||||
foreach ($comments as &$comment) {
|
||||
if ($comment['id'] === $id) {
|
||||
$comment['readby'] = $comment['readby'] ?? [];
|
||||
if (!in_array($userUuid, $comment['readby'])) {
|
||||
$comment['readby'][] = $userUuid;
|
||||
$updated = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($updated) {
|
||||
$file->update(['comments' => $comments]);
|
||||
}
|
||||
|
||||
return $updated;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
<?php
|
||||
|
||||
namespace adrienpayet\notifications\providers;
|
||||
|
||||
use adrienpayet\notifications\NotificationProvider;
|
||||
use Kirby\Cms\Page;
|
||||
use Kirby\Cms\User;
|
||||
use Kirby\Data\Yaml;
|
||||
|
||||
/**
|
||||
* Provider pour les notifications de type "content".
|
||||
* Dérivé depuis les briefs validés (isValidated = true).
|
||||
*/
|
||||
class ContentProvider implements NotificationProvider
|
||||
{
|
||||
public function getType(): string
|
||||
{
|
||||
return 'content';
|
||||
}
|
||||
|
||||
public function collect(Page $project, User $user): array
|
||||
{
|
||||
$notifications = [];
|
||||
$userUuid = (string) $user->uuid();
|
||||
|
||||
// Chercher les briefs validés (client-brief et extended-brief)
|
||||
$briefTemplates = ['client-brief', 'extended-brief'];
|
||||
|
||||
foreach ($project->children() as $step) {
|
||||
if (!in_array($step->intendedTemplate()->name(), $briefTemplates)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Pas de notification si le brief n'est pas validé
|
||||
if ($step->isValidated()->isFalse()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Vérifier que les champs requis existent
|
||||
if ($step->validatedBy()->isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$authorUuid = $step->validatedBy()->value();
|
||||
|
||||
// Ne pas notifier l'auteur de sa propre validation
|
||||
if ($authorUuid === $userUuid) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$readby = $step->validationReadby()->isNotEmpty()
|
||||
? Yaml::decode($step->validationReadby()->value())
|
||||
: [];
|
||||
|
||||
if (!is_array($readby)) {
|
||||
$readby = [];
|
||||
}
|
||||
|
||||
$stepLabel = $step->intendedTemplate()->name() === 'client-brief'
|
||||
? 'Brief client'
|
||||
: 'Brief étendu';
|
||||
|
||||
$notifications[] = [
|
||||
'id' => 'content-' . (string) $step->uuid(),
|
||||
'type' => 'content',
|
||||
'text' => "Nouveau $stepLabel validé",
|
||||
'author' => [
|
||||
'uuid' => $authorUuid,
|
||||
'name' => $step->validatedByName()->value() ?? '',
|
||||
'email' => $step->validatedByEmail()->value() ?? '',
|
||||
'role' => 'client',
|
||||
],
|
||||
'date' => $step->validatedAt()->value() ?? '',
|
||||
'location' => [
|
||||
'page' => [
|
||||
'uri' => $step->uri(),
|
||||
'title' => (string) $step->title(),
|
||||
'template' => $step->intendedTemplate()->name(),
|
||||
],
|
||||
'project' => [
|
||||
'uri' => $project->uri(),
|
||||
'title' => (string) $project->title(),
|
||||
]
|
||||
],
|
||||
'readby' => $readby,
|
||||
'isRead' => in_array($userUuid, $readby),
|
||||
'_briefUri' => $step->uri(),
|
||||
];
|
||||
}
|
||||
|
||||
return $notifications;
|
||||
}
|
||||
|
||||
public function markAsRead(string $id, array $location, User $user): bool
|
||||
{
|
||||
$briefUri = $location['_briefUri'] ?? null;
|
||||
if (!$briefUri) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$brief = page($briefUri);
|
||||
if (!$brief) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$readby = $brief->validationReadby()->isNotEmpty()
|
||||
? Yaml::decode($brief->validationReadby()->value())
|
||||
: [];
|
||||
|
||||
if (!is_array($readby)) {
|
||||
$readby = [];
|
||||
}
|
||||
|
||||
$userUuid = (string) $user->uuid();
|
||||
|
||||
if (in_array($userUuid, $readby)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$readby[] = $userUuid;
|
||||
|
||||
$brief->update([
|
||||
'validationReadby' => array_unique($readby)
|
||||
]);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
<?php
|
||||
|
||||
namespace adrienpayet\notifications\providers;
|
||||
|
||||
use adrienpayet\notifications\NotificationProvider;
|
||||
use Kirby\Cms\Page;
|
||||
use Kirby\Cms\User;
|
||||
use Kirby\Data\Yaml;
|
||||
|
||||
/**
|
||||
* Provider pour les notifications de type "project-request".
|
||||
* Dérivé depuis les champs du projet quand isClientRequest est true.
|
||||
*/
|
||||
class ProjectRequestProvider implements NotificationProvider
|
||||
{
|
||||
public function getType(): string
|
||||
{
|
||||
return 'project-request';
|
||||
}
|
||||
|
||||
public function collect(Page $project, User $user): array
|
||||
{
|
||||
// Pas de notification si ce n'est pas une demande client
|
||||
if ($project->isClientRequest()->isFalse()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Vérifier que les champs requis existent
|
||||
if ($project->requestAuthor()->isEmpty()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$userUuid = (string) $user->uuid();
|
||||
$authorUuid = $project->requestAuthor()->value();
|
||||
|
||||
// Ne pas notifier l'auteur de sa propre demande
|
||||
if ($authorUuid === $userUuid) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$readby = $project->requestReadby()->isNotEmpty()
|
||||
? Yaml::decode($project->requestReadby()->value())
|
||||
: [];
|
||||
|
||||
if (!is_array($readby)) {
|
||||
$readby = [];
|
||||
}
|
||||
|
||||
return [[
|
||||
'id' => 'project-request-' . (string) $project->uuid(),
|
||||
'type' => 'project-request',
|
||||
'text' => $project->requestDetails()->value() ?? '',
|
||||
'author' => [
|
||||
'uuid' => $authorUuid,
|
||||
'name' => $project->requestAuthorName()->value() ?? '',
|
||||
'email' => $project->requestAuthorEmail()->value() ?? '',
|
||||
'role' => 'client',
|
||||
],
|
||||
'date' => $project->requestDate()->value() ?? '',
|
||||
'location' => [
|
||||
'page' => [
|
||||
'uri' => $project->uri(),
|
||||
'title' => (string) $project->title(),
|
||||
'template' => 'project',
|
||||
],
|
||||
'project' => [
|
||||
'uri' => $project->uri(),
|
||||
'title' => (string) $project->title(),
|
||||
]
|
||||
],
|
||||
'readby' => $readby,
|
||||
'isRead' => in_array($userUuid, $readby),
|
||||
'_projectUri' => $project->uri(),
|
||||
]];
|
||||
}
|
||||
|
||||
public function markAsRead(string $id, array $location, User $user): bool
|
||||
{
|
||||
$projectUri = $location['_projectUri'] ?? null;
|
||||
if (!$projectUri) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$project = page($projectUri);
|
||||
if (!$project) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$readby = $project->requestReadby()->isNotEmpty()
|
||||
? Yaml::decode($project->requestReadby()->value())
|
||||
: [];
|
||||
|
||||
if (!is_array($readby)) {
|
||||
$readby = [];
|
||||
}
|
||||
|
||||
$userUuid = (string) $user->uuid();
|
||||
|
||||
if (in_array($userUuid, $readby)) {
|
||||
return true; // Déjà lu
|
||||
}
|
||||
|
||||
$readby[] = $userUuid;
|
||||
|
||||
$project->update([
|
||||
'requestReadby' => array_unique($readby)
|
||||
]);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
<?php
|
||||
|
||||
namespace adrienpayet\notifications\providers;
|
||||
|
||||
use adrienpayet\notifications\NotificationProvider;
|
||||
use Kirby\Cms\Page;
|
||||
use Kirby\Cms\User;
|
||||
use Kirby\Data\Yaml;
|
||||
|
||||
/**
|
||||
* Provider pour les notifications de type "comment-reply".
|
||||
* Collecte les réponses aux commentaires depuis les fichiers.
|
||||
*/
|
||||
class ReplyProvider implements NotificationProvider
|
||||
{
|
||||
public function getType(): string
|
||||
{
|
||||
return 'comment-reply';
|
||||
}
|
||||
|
||||
public function collect(Page $project, User $user): array
|
||||
{
|
||||
$notifications = [];
|
||||
$userUuid = (string) $user->uuid();
|
||||
|
||||
// Parcourir toutes les étapes du projet
|
||||
foreach ($project->children() as $step) {
|
||||
$this->collectFromPage($step, $project, $userUuid, $notifications);
|
||||
|
||||
// Parcourir aussi les sous-pages (ex: tracks)
|
||||
foreach ($step->children() as $subPage) {
|
||||
$this->collectFromPage($subPage, $project, $userUuid, $notifications);
|
||||
}
|
||||
}
|
||||
|
||||
return $notifications;
|
||||
}
|
||||
|
||||
private function collectFromPage(Page $page, Page $project, string $userUuid, array &$notifications): void
|
||||
{
|
||||
foreach ($page->files() as $file) {
|
||||
if ($file->comments()->isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$comments = Yaml::decode($file->comments()->value());
|
||||
if (!is_array($comments)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($comments as $comment) {
|
||||
$replies = $comment['replies'] ?? [];
|
||||
|
||||
foreach ($replies as $reply) {
|
||||
// Ne pas notifier l'auteur de sa propre réponse
|
||||
$authorUuid = $reply['author']['uuid'] ?? '';
|
||||
if ($authorUuid === $userUuid) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$readby = $reply['readby'] ?? [];
|
||||
|
||||
$location = $reply['location'] ?? $comment['location'] ?? [];
|
||||
// Assurer que location.project existe toujours
|
||||
if (!isset($location['project'])) {
|
||||
$location['project'] = [
|
||||
'uri' => $project->uri(),
|
||||
'title' => (string) $project->title(),
|
||||
];
|
||||
}
|
||||
|
||||
$notifications[] = [
|
||||
'id' => $reply['id'],
|
||||
'type' => 'comment-reply',
|
||||
'text' => $reply['text'] ?? '',
|
||||
'author' => $reply['author'] ?? [],
|
||||
'date' => $reply['date'] ?? '',
|
||||
'location' => $location,
|
||||
'position' => $reply['position'] ?? $comment['position'] ?? [],
|
||||
'readby' => $readby,
|
||||
'isRead' => in_array($userUuid, $readby),
|
||||
// Métadonnées pour markAsRead
|
||||
'_file' => (string) $file->uuid(),
|
||||
'_parentCommentId' => $comment['id'],
|
||||
'_stepUri' => $page->uri(),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function markAsRead(string $id, array $location, User $user): bool
|
||||
{
|
||||
$fileUuid = $location['_file'] ?? null;
|
||||
$parentCommentId = $location['_parentCommentId'] ?? null;
|
||||
|
||||
if (!$fileUuid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Trouver le fichier par UUID (peut être avec ou sans préfixe file://)
|
||||
$fileUri = str_starts_with($fileUuid, 'file://') ? $fileUuid : 'file://' . $fileUuid;
|
||||
$file = kirby()->file($fileUri);
|
||||
if (!$file) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$comments = Yaml::decode($file->comments()->value());
|
||||
if (!is_array($comments)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$userUuid = (string) $user->uuid();
|
||||
$updated = false;
|
||||
|
||||
foreach ($comments as &$comment) {
|
||||
// Si on a l'ID du parent, l'utiliser pour cibler
|
||||
if ($parentCommentId && $comment['id'] !== $parentCommentId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$replies = &$comment['replies'] ?? [];
|
||||
|
||||
foreach ($replies as &$reply) {
|
||||
if ($reply['id'] === $id) {
|
||||
$reply['readby'] = $reply['readby'] ?? [];
|
||||
if (!in_array($userUuid, $reply['readby'])) {
|
||||
$reply['readby'][] = $userUuid;
|
||||
$updated = true;
|
||||
}
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($updated) {
|
||||
$file->update(['comments' => $comments]);
|
||||
}
|
||||
|
||||
return $updated;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue