Globinours/app/Controllers/GrantsController.php

240 lines
9.8 KiB
PHP
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
final class GrantsController
{
private static function admin(): void
{
$action = ($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST' ? 'edit' : 'view';
if (!PermissionService::can('grants', $action)) {
http_response_code(403);
echo h(t('grant.forbidden'));
exit();
}
}
private static function year(): int
{
return max(2000, min(2200, (int) ($_REQUEST['year'] ?? date('Y'))));
}
public static function index(): void
{
self::admin();
$year = self::year();
$applications = GrantService::applications($year);
render('admin_grants.php', [
'title' => t('grant.title'),
'year' => $year,
'annual' => GrantService::ensureYear($year),
'stats' => GrantService::statistics($year),
'details' => GrantService::detailedStatistics($year),
'comparison' => GrantService::comparison($year),
'applications' => $applications,
'checklist' => GrantService::checklist($year),
'documents' => GrantService::documents($year),
'saved' => isset($_GET['saved']),
'error' => (string) ($_GET['error'] ?? ''),
]);
}
public static function saveAnnual(): void
{
self::admin();
$year = self::year();
GrantService::ensureYear($year);
$ints = ['members_count', 'volunteers_count', 'employees_count'];
$nums = [
'employees_fte',
'treasury_amount',
'actual_expenses',
'actual_income',
'forecast_expenses',
'forecast_income',
'voluntary_contributions',
];
$texts = [
'annual_report',
'completed_actions',
'planned_actions',
'beneficiaries',
'officers_notes',
'in_kind_support',
];
$values = [
':year' => $year,
':last_ag_date' => self::date($_POST['last_ag_date'] ?? null),
':uid' => Auth::id(),
];
foreach ($ints as $f) {
$values[':' . $f] = max(0, (int) ($_POST[$f] ?? 0));
}
foreach ($nums as $f) {
$values[':' . $f] = max(0, self::amount($_POST[$f] ?? 0));
}
foreach ($texts as $f) {
$values[':' . $f] = trim((string) ($_POST[$f] ?? ''));
}
$sets = [];
foreach (array_merge($ints, $nums, $texts, ['last_ag_date']) as $f) {
$sets[] = "$f=:$f";
}
$sql =
'UPDATE grant_years SET ' .
implode(',', $sets) .
',updated_by=:uid,updated_at=datetime(\'now\') WHERE year=:year';
DB::pdo()->prepare($sql)->execute($values);
AuditService::log('grant_year_saved', '/admin/grants/save', 'Bilan annuel ' . $year . ' mis à jour');
header('Location: /admin/grants?year=' . $year . '&saved=1');
}
public static function saveApplication(): void
{
self::admin();
$year = self::year();
GrantService::ensureYear($year);
$id = (int) ($_POST['application_id'] ?? 0);
$existing = [];
if ($id) {
$q = DB::pdo()->prepare('SELECT * FROM grant_applications WHERE id=? AND year=?');
$q->execute([$id, $year]);
$existing = $q->fetch() ?: [];
}
$name = trim((string) ($_POST['organization_name'] ?? ($existing['organization_name'] ?? '')));
if ($name === '') {
header('Location: /admin/grants?year=' . $year . '&error=organisme');
return;
}
$status = (string) ($_POST['status'] ?? ($existing['status'] ?? 'draft'));
if (!isset(GrantService::STATUSES[$status])) {
$status = 'draft';
}
$awardedRaw = $_POST['awarded_amount'] ?? ($existing['awarded_amount'] ?? '');
$params = [
':year' => $year,
':name' => $name,
':deadline' => self::date($_POST['deadline'] ?? ($existing['deadline'] ?? null)),
':requested' => self::amount($_POST['requested_amount'] ?? ($existing['requested_amount'] ?? 0)),
':previous' => self::amount($_POST['previous_amount'] ?? ($existing['previous_amount'] ?? 0)),
':awarded' => trim((string) $awardedRaw) === '' ? null : self::amount($awardedRaw),
':status' => $status,
':notes' => trim((string) ($_POST['notes'] ?? ($existing['notes'] ?? ''))),
':uid' => Auth::id(),
];
if ($id) {
$params[':id'] = $id;
DB::pdo()
->prepare(
'UPDATE grant_applications SET organization_name=:name,deadline=:deadline,requested_amount=:requested,previous_amount=:previous,awarded_amount=:awarded,status=:status,notes=:notes,updated_by=:uid,updated_at=datetime(\'now\') WHERE id=:id AND year=:year',
)
->execute($params);
} else {
DB::pdo()
->prepare(
'INSERT INTO grant_applications(year,organization_name,deadline,requested_amount,previous_amount,awarded_amount,status,notes,created_by,updated_by) VALUES(:year,:name,:deadline,:requested,:previous,:awarded,:status,:notes,:uid,:uid)',
)
->execute($params);
}
header('Location: /admin/grants?year=' . $year . '&saved=1');
}
public static function upload(): void
{
self::admin();
$year = self::year();
GrantService::ensureYear($year);
$f = $_FILES['document'] ?? null;
if (!$f || ($f['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK || ($f['size'] ?? 0) > 20_000_000) {
header('Location: /admin/grants?year=' . $year . '&error=document');
return;
}
$mime = (new finfo(FILEINFO_MIME_TYPE))->file($f['tmp_name']) ?: 'application/octet-stream';
$allowed = ['application/pdf' => 'pdf', 'application/vnd.oasis.opendocument.text' => 'odt'];
if (!isset($allowed[$mime])) {
header('Location: /admin/grants?year=' . $year . '&error=format');
return;
}
$dir = dirname(__DIR__, 2) . '/data/grants';
if (!is_dir($dir)) {
mkdir($dir, 0770, true);
}
$stored = bin2hex(random_bytes(18)) . '.' . $allowed[$mime];
if (!move_uploaded_file($f['tmp_name'], $dir . '/' . $stored)) {
throw new RuntimeException(t('error.document_storage'));
}
$category = (string) ($_POST['category'] ?? 'other');
if (!isset(GrantService::CATEGORIES[$category])) {
$category = 'other';
}
$global = isset($_POST['global_document']);
$applicationId = (int) ($_POST['application_id'] ?? 0);
if ($applicationId) {
$check = DB::pdo()->prepare('SELECT 1 FROM grant_applications WHERE id=? AND year=?');
$check->execute([$applicationId, $year]);
if (!$check->fetchColumn()) {
$applicationId = 0;
}
}
if ($category === 'official_form' && !$applicationId) {
@unlink($dir . '/' . $stored);
header('Location: /admin/grants?year=' . $year . '&error=organisme');
return;
}
DB::pdo()
->prepare(
'INSERT INTO grant_documents(year,application_id,category,title,stored_name,original_name,mime_type,size_bytes,valid_until,notes,uploaded_by) VALUES(?,?,?,?,?,?,?,?,?,?,?)',
)
->execute([
$global ? null : $year,
$applicationId ?: null,
$category,
trim((string) ($_POST['title'] ?? '')) ?: pathinfo((string) $f['name'], PATHINFO_FILENAME),
$stored,
basename((string) $f['name']),
$mime,
(int) $f['size'],
self::date($_POST['valid_until'] ?? null),
trim((string) ($_POST['notes'] ?? '')),
Auth::id(),
]);
header('Location: /admin/grants?year=' . $year . '&saved=1');
}
public static function download(): void
{
self::admin();
$s = DB::pdo()->prepare('SELECT * FROM grant_documents WHERE id=?');
$s->execute([(int) ($_GET['id'] ?? 0)]);
$d = $s->fetch();
$path = dirname(__DIR__, 2) . '/data/grants/' . ($d['stored_name'] ?? '');
if (!$d || !is_file($path)) {
http_response_code(404);
echo h(t('grant.document_not_found'));
return;
}
PrivacyService::logAccess('grant_document', (int) $d['id'], null, (string) $d['original_name']);
header('Content-Type: ' . $d['mime_type']);
header('Content-Disposition: inline; filename="' . rawurlencode($d['original_name']) . '"');
header('Content-Length: ' . filesize($path));
header('Cache-Control: private,no-store');
readfile($path);
}
public static function print(): void
{
self::admin();
$year = self::year();
$annual = GrantService::ensureYear($year);
$stats = GrantService::statistics($year);
$details = GrantService::detailedStatistics($year);
$comparison = GrantService::comparison($year);
$applications = GrantService::applications($year);
$documents = GrantService::documents($year);
$generatedAt = new DateTimeImmutable('now', new DateTimeZone('Europe/Paris'));
header('Content-Type: text/html; charset=UTF-8');
require __DIR__ . '/../Views/admin_grants_print.php';
}
private static function date(mixed $v): ?string
{
$v = trim((string) $v);
return preg_match('/^\d{4}-\d{2}-\d{2}$/', $v) ? $v : null;
}
private static function amount(mixed $v): float
{
return round((float) str_replace([' ', ' ', ','], ['', '', '.'], (string) $v), 2);
}
}