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,240 @@
<?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);
}
}