240 lines
9.8 KiB
PHP
240 lines
9.8 KiB
PHP
<?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);
|
||
}
|
||
}
|