Publier Globinours 1.0.0-rc.3

This commit is contained in:
Alexandre NOEL 2026-09-03 12:39:15 +02:00
commit 9a2b4068da
325 changed files with 38230 additions and 20 deletions

View file

@ -0,0 +1,267 @@
<?php
declare(strict_types=1);
final class AgendaController
{
public static function index(): void
{
$month = (string) ($_GET['month'] ?? date('Y-m'));
if (!preg_match('/^\d{4}-\d{2}$/', $month)) {
$month = date('Y-m');
}
$cursor =
DateTimeImmutable::createFromFormat('!Y-m', $month) ?: new DateTimeImmutable('first day of this month');
$date = (string) ($_GET['date'] ?? date('Y-m-d'));
$weekCursor = self::date($date) ?: new DateTimeImmutable('today');
$db = DB::pdo();
$user = Auth::user();
$requestedView = (string) ($_GET['view'] ?? '');
$viewMode = in_array($requestedView, ['month', 'week'], true)
? $requestedView
: (string) ($_SESSION['agenda_view'] ?? ($user['agenda_view'] ?? 'month'));
if (!in_array($viewMode, ['month', 'week'], true)) {
$viewMode = 'month';
}
if ($requestedView !== '') {
$_SESSION['agenda_view'] = $viewMode;
}
$filters = self::filters();
$items = self::filtered(
$viewMode === 'week' ? AgendaService::week($weekCursor) : AgendaService::month($cursor),
$filters,
(int) $user['id'],
);
$upcoming = self::filtered(AgendaService::upcoming(), $filters, (int) $user['id']);
$feed = $db->prepare('SELECT agenda_feed_token_hash,agenda_feed_created_at FROM users WHERE id=?');
$feed->execute([(int) $user['id']]);
$feedState = $feed->fetch(PDO::FETCH_ASSOC) ?: [];
$token = (string) ($_SESSION['agenda_feed_token_once'] ?? '');
unset($_SESSION['agenda_feed_token_once']);
$volunteers = $db
->query(
"SELECT DISTINCT dc.id,dc.name FROM directory_contacts dc JOIN directory_contact_roles r ON r.contact_id=dc.id AND r.role='benevole' WHERE dc.deleted_at IS NULL ORDER BY dc.name COLLATE NOCASE",
)
->fetchAll(PDO::FETCH_ASSOC);
$edit = (int) ($_GET['edit'] ?? 0);
render('agenda.php', [
'title' => t('agenda.title'),
'cursor' => $cursor,
'weekCursor' => $weekCursor,
'viewMode' => $viewMode,
'items' => $items,
'upcoming' => $upcoming,
'filters' => $filters,
'filterSuffix' => self::filterSuffix($filters),
'feedEnabled' => !empty($feedState['agenda_feed_token_hash']),
'feedCreatedAt' => $feedState['agenda_feed_created_at'] ?? null,
'feedUrl' => $token !== '' ? CalendarFeedService::absoluteUrl('/agenda/feed.ics?token=' . $token) : '',
'editItem' => $edit ? AgendaService::find($edit) : null,
'animals' => $db
->query(
'SELECT id,name FROM animals WHERE deleted_at IS NULL AND archived_at IS NULL ORDER BY name COLLATE NOCASE',
)
->fetchAll(),
'contacts' => $db
->query('SELECT id,name FROM directory_contacts WHERE deleted_at IS NULL ORDER BY name COLLATE NOCASE')
->fetchAll(),
'volunteers' => $volunteers,
'users' => $db
->query(
"SELECT id,COALESCE(NULLIF(display_name,''),username) name FROM users WHERE active=1 ORDER BY name COLLATE NOCASE",
)
->fetchAll(),
'status' => (string) ($_GET['status'] ?? ''),
'error' => (string) ($_GET['error'] ?? ''),
]);
}
public static function save(): void
{
try {
$id = (int) ($_POST['id'] ?? 0);
$saved = AgendaService::save($_POST, $id ?: null);
$conflicts = AgendaService::conflicts($saved);
AuditService::log(
$id ? 'agenda_updated' : 'agenda_created',
'/agenda/save',
$id ? 'Événement modifié' : 'Événement créé',
'agenda',
$saved,
['conflicts' => count($conflicts)],
);
header('Location: /agenda?status=' . ($conflicts ? 'conflict' : 'saved'));
} catch (Throwable $e) {
header('Location: /agenda?error=' . rawurlencode(SecurityService::publicError($e)));
}
exit();
}
public static function status(): void
{
try {
$id = (int) ($_POST['id'] ?? 0);
AgendaService::setStatus($id, (string) ($_POST['status'] ?? ''));
AuditService::log('agenda_status', '/agenda/status', 'État de lagenda modifié', 'agenda', $id, [
'status' => $_POST['status'] ?? '',
]);
header('Location: /agenda?status=updated');
} catch (Throwable $e) {
header('Location: /agenda?error=' . rawurlencode(SecurityService::publicError($e)));
}
exit();
}
public static function export(): void
{
$view = (string) ($_GET['view'] ?? 'month');
if ($view === 'week') {
$date = self::date((string) ($_GET['date'] ?? '')) ?: new DateTimeImmutable('today');
$items = AgendaService::week($date);
$name = 'agenda-semaine-' . $date->modify('monday this week')->format('Y-m-d') . '.ics';
} else {
$month = (string) ($_GET['month'] ?? date('Y-m'));
$date = preg_match('/^\d{4}-\d{2}$/', $month) ? DateTimeImmutable::createFromFormat('!Y-m', $month) : false;
$date = $date ?: new DateTimeImmutable('first day of this month');
$items = AgendaService::month($date);
$name = 'agenda-' . $date->format('Y-m') . '.ics';
}
$items = self::filtered($items, self::filters(), (int) Auth::id());
self::output(
CalendarFeedService::render($items, AppSettings::get('association_name') . ' — ' . t('agenda.title')),
$name,
);
}
public static function feed(): void
{
$user = CalendarFeedService::userForToken((string) ($_GET['token'] ?? ''));
if (!$user) {
http_response_code(404);
header('Content-Type: text/plain; charset=utf-8');
echo t('agenda.feed_invalid');
return;
}
$items = AgendaService::range(new DateTimeImmutable('-90 days'), new DateTimeImmutable('+365 days 23:59:59'));
self::output(
CalendarFeedService::render($items, AppSettings::get('association_name') . ' — ' . t('agenda.title')),
'globinours-agenda.ics',
false,
);
}
public static function regenerateFeed(): void
{
self::feedPost();
$_SESSION['agenda_feed_token_once'] = CalendarFeedService::generateToken((int) Auth::id());
AuditService::log('agenda_feed_regenerated', '/agenda/feed/regenerate', 'Lien privé dagenda régénéré');
header('Location: /agenda?status=feed');
exit();
}
public static function revokeFeed(): void
{
self::feedPost();
CalendarFeedService::revoke((int) Auth::id());
AuditService::log('agenda_feed_revoked', '/agenda/feed/revoke', 'Lien privé dagenda révoqué');
header('Location: /agenda?status=feed-revoked');
exit();
}
private static function filters(): array
{
$types = [...AgendaService::TYPES, 'vaccine', 'deworming', 'treatment_start', 'treatment_end'];
$scope = ($_GET['scope'] ?? 'all') === 'mine' ? 'mine' : 'all';
$type = in_array($t = (string) ($_GET['category'] ?? ''), $types, true) ? $t : '';
return [
'scope' => $scope,
'assigned_user_id' => max(0, (int) ($_GET['assigned_user_id'] ?? 0)),
'volunteer_contact_id' => max(0, (int) ($_GET['volunteer_contact_id'] ?? 0)),
'category' => $type,
'show_medical' => (string) ($_GET['show_medical'] ?? '1') !== '0',
'q' => mb_substr(trim((string) ($_GET['q'] ?? '')), 0, 100),
];
}
private static function filtered(array $items, array $f, int $userId): array
{
return array_values(
array_filter($items, static function ($row) use ($f, $userId): bool {
$source = (string) ($row['source_type'] ?? 'agenda');
if ($f['scope'] === 'mine' && (int) ($row['assigned_user_id'] ?? 0) !== $userId) {
return false;
}
if ($f['assigned_user_id'] && (int) ($row['assigned_user_id'] ?? 0) !== $f['assigned_user_id']) {
return false;
}
if (
$f['volunteer_contact_id'] &&
!in_array(
$f['volunteer_contact_id'],
array_map('intval', array_filter(explode(',', (string) ($row['volunteer_ids'] ?? '')))),
true,
)
) {
return false;
}
if ($f['category'] !== '' && (string) $row['item_type'] !== $f['category']) {
return false;
}
if (!$f['show_medical'] && $source !== 'agenda') {
return false;
}
if (
$f['q'] !== '' &&
!str_contains(
mb_strtolower(
implode(' ', [
(string) ($row['title'] ?? ''),
(string) ($row['animal_name'] ?? ''),
(string) ($row['location'] ?? ''),
(string) ($row['assigned_name'] ?? ''),
(string) ($row['volunteer_names'] ?? ''),
]),
),
mb_strtolower($f['q']),
)
) {
return false;
}
return true;
}),
);
}
private static function filterSuffix(array $f): string
{
$query = http_build_query(
[
'scope' => $f['scope'],
'assigned_user_id' => $f['assigned_user_id'] ?: null,
'volunteer_contact_id' => $f['volunteer_contact_id'] ?: null,
'category' => $f['category'] ?: null,
'show_medical' => $f['show_medical'] ? '1' : '0',
'q' => $f['q'] ?: null,
],
'',
'&',
PHP_QUERY_RFC3986,
);
return $query !== '' ? '&amp;' . str_replace('&', '&amp;', $query) : '';
}
private static function feedPost(): void
{
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST' || !Auth::validCsrf($_POST['csrf'] ?? null)) {
http_response_code(419);
exit();
}
if (!Auth::user() || !PermissionService::can('agenda')) {
http_response_code(403);
exit();
}
}
private static function output(string $content, string $filename, bool $attachment = true): void
{
header('Content-Type: text/calendar; charset=utf-8');
header('Content-Disposition: ' . ($attachment ? 'attachment' : 'inline') . '; filename="' . $filename . '"');
header('Cache-Control: private, no-store');
echo $content;
exit();
}
private static function date(string $value): ?DateTimeImmutable
{
$date = DateTimeImmutable::createFromFormat('!Y-m-d', $value);
return $date && $date->format('Y-m-d') === $value ? $date : null;
}
}