Publier Globinours 1.0.0-rc.3
This commit is contained in:
parent
ea8c24d622
commit
9a2b4068da
325 changed files with 38230 additions and 20 deletions
267
app/Controllers/AgendaController.php
Normal file
267
app/Controllers/AgendaController.php
Normal 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 l’agenda 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é d’agenda 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é d’agenda 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 !== '' ? '&' . str_replace('&', '&', $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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue