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,334 @@
<?php
declare(strict_types=1);
final class MedicalDocumentsController
{
private const MAX_BYTES = 20 * 1024 * 1024;
private const TYPES = [
'application/pdf' => 'pdf',
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/webp' => 'webp',
];
public static function savePrescription(): void
{
$animalId = self::animalId();
$date = self::date((string) ($_POST['prescribed_on'] ?? ''));
if (!$animalId || !$date) {
self::fail(t('medical_document.invalid_data'));
return;
}
try {
$file = self::storeUpload('document', $animalId, 'prescription', true);
} catch (Throwable $e) {
self::fail($e->getMessage());
return;
}
$db = DB::pdo();
try {
$db->beginTransaction();
$stmt = $db->prepare(
'INSERT INTO medical_prescriptions(animal_id,prescribed_on,veterinarian_contact_id,clinic_contact_id,status,notes,original_name,stored_name,mime_type,size_bytes,created_by) VALUES(?,?,?,?,?,?,?,?,?,?,?)',
);
$status = in_array($_POST['status'] ?? '', ['active', 'finished', 'replaced'], true)
? $_POST['status']
: 'active';
$stmt->execute([
$animalId,
$date,
self::contact('veterinarian_contact_id'),
self::contact('clinic_contact_id'),
$status,
self::text('notes'),
$file['original'],
$file['stored'],
$file['mime'],
$file['size'],
Auth::id(),
]);
$id = (int) $db->lastInsertId();
$link = $db->prepare(
'INSERT OR IGNORE INTO prescription_treatments(prescription_id,treatment_id) SELECT ?,id FROM treatments WHERE id=? AND animal_id=?',
);
foreach ((array) ($_POST['treatment_ids'] ?? []) as $treatmentId) {
$link->execute([$id, (int) $treatmentId, $animalId]);
}
self::history($db, $animalId, 'Ordonnance ajoutée', self::fr($date));
$db->commit();
AuditService::log(
'prescription_added',
'/animal/prescription/save',
t('medical_document.prescription_added'),
'animal',
$animalId,
['prescription_id' => $id],
);
} catch (Throwable $e) {
if ($db->inTransaction()) {
$db->rollBack();
}
self::remove($animalId, $file['stored']);
throw $e;
}
self::back($animalId);
}
public static function saveLabReport(): void
{
$animalId = self::animalId();
$date = self::date((string) ($_POST['sampled_on'] ?? ''));
$names = (array) ($_POST['parameter_name'] ?? []);
$values = (array) ($_POST['value'] ?? []);
if (!$animalId || !$date) {
self::fail(t('medical_document.invalid_data'));
return;
}
try {
$file = self::storeUpload('document', $animalId, 'analysis', false);
} catch (Throwable $e) {
self::fail($e->getMessage());
return;
}
$rows = [];
foreach ($names as $i => $name) {
$name = trim((string) $name);
$raw = str_replace(',', '.', trim((string) ($values[$i] ?? '')));
if ($name === '' && $raw === '') {
continue;
}
if ($name === '' || !is_numeric($raw)) {
if ($file) {
self::remove($animalId, $file['stored']);
}
self::fail(t('medical_document.invalid_result'));
return;
}
$min = self::numberOrNull(($_POST['reference_min'] ?? [])[$i] ?? null);
$max = self::numberOrNull(($_POST['reference_max'] ?? [])[$i] ?? null);
if ($min !== null && $max !== null && $min > $max) {
if ($file) {
self::remove($animalId, $file['stored']);
}
self::fail(t('medical_document.invalid_result'));
return;
}
$rows[] = [
'name' => mb_substr($name, 0, 120),
'value' => (float) $raw,
'unit' => self::arrayText('unit', $i, 30),
'min' => $min,
'max' => $max,
'notes' => self::arrayText('result_notes', $i, 255),
];
}
if (!$rows && !$file) {
self::fail(t('medical_document.result_or_document_required'));
return;
}
$db = DB::pdo();
try {
$db->beginTransaction();
$stmt = $db->prepare(
'INSERT INTO lab_reports(animal_id,sampled_on,report_type,laboratory_name,veterinarian_contact_id,clinic_contact_id,notes,original_name,stored_name,mime_type,size_bytes,created_by) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)',
);
$stmt->execute([
$animalId,
$date,
self::text('report_type', 120),
self::text('laboratory_name', 150),
self::contact('veterinarian_contact_id'),
self::contact('clinic_contact_id'),
self::text('notes'),
$file['original'] ?? null,
$file['stored'] ?? null,
$file['mime'] ?? null,
$file['size'] ?? 0,
Auth::id(),
]);
$reportId = (int) $db->lastInsertId();
$insert = $db->prepare(
'INSERT INTO lab_results(report_id,parameter_name,value,unit,reference_min,reference_max,notes,position) VALUES(?,?,?,?,?,?,?,?)',
);
foreach ($rows as $i => $row) {
$insert->execute([
$reportId,
$row['name'],
$row['value'],
$row['unit'],
$row['min'],
$row['max'],
$row['notes'],
$i,
]);
}
self::history(
$db,
$animalId,
'Résultats danalyse ajoutés',
self::fr($date) . ($rows ? ' · ' . count($rows) . ' paramètre(s)' : ''),
);
$db->commit();
AuditService::log(
'lab_report_added',
'/animal/lab-report/save',
t('medical_document.analysis_added'),
'animal',
$animalId,
['report_id' => $reportId, 'results' => count($rows)],
);
} catch (Throwable $e) {
if ($db->inTransaction()) {
$db->rollBack();
}
if ($file) {
self::remove($animalId, $file['stored']);
}
throw $e;
}
self::back($animalId);
}
public static function download(): void
{
$id = (int) ($_GET['id'] ?? 0);
$kind = (string) ($_GET['kind'] ?? '');
$table = $kind === 'prescription' ? 'medical_prescriptions' : ($kind === 'analysis' ? 'lab_reports' : '');
if (!$id || !$table) {
http_response_code(404);
return;
}
$stmt = DB::pdo()->prepare(
"SELECT d.animal_id,d.original_name,d.stored_name,d.mime_type FROM $table d JOIN animals a ON a.id=d.animal_id WHERE d.id=? AND a.deleted_at IS NULL",
);
$stmt->execute([$id]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$row || !$row['stored_name']) {
http_response_code(404);
return;
}
$path = self::directory((int) $row['animal_id']) . '/' . basename((string) $row['stored_name']);
if (!is_file($path)) {
http_response_code(404);
return;
}
PrivacyService::logAccess('medical_' . $kind, $id, (int) $row['animal_id'], (string) $row['original_name']);
AuditService::log(
'private_file_viewed',
'/animal/medical-document',
'Document médical privé consulté',
'animal',
(int) $row['animal_id'],
['kind' => $kind, 'document_id' => $id],
);
header('Content-Type: ' . ($row['mime_type'] ?: 'application/octet-stream'));
header('Content-Length: ' . filesize($path));
header("Content-Disposition: inline; filename*=UTF-8''" . rawurlencode((string) $row['original_name']));
header('Cache-Control: private, no-store');
readfile($path);
}
private static function animalId(): int
{
$id = (int) ($_POST['animal_id'] ?? 0);
if (!$id) {
return 0;
}
$s = DB::pdo()->prepare('SELECT 1 FROM animals WHERE id=? AND deleted_at IS NULL');
$s->execute([$id]);
return $s->fetchColumn() ? $id : 0;
}
private static function date(string $v): ?string
{
$d = DateTimeImmutable::createFromFormat('!Y-m-d', $v);
return $d && $d->format('Y-m-d') === $v ? $v : null;
}
private static function contact(string $key): ?int
{
$id = (int) ($_POST[$key] ?? 0);
if ($id <= 0) {
return null;
}
$role = $key === 'veterinarian_contact_id' ? 'veterinaire' : 'cabinet';
$s = DB::pdo()->prepare(
'SELECT 1 FROM directory_contacts dc JOIN directory_contact_roles r ON r.contact_id=dc.id WHERE dc.id=? AND dc.deleted_at IS NULL AND r.role=?',
);
$s->execute([$id, $role]);
return $s->fetchColumn() ? $id : null;
}
private static function text(string $key, int $max = 2000): ?string
{
$v = trim((string) ($_POST[$key] ?? ''));
return $v === '' ? null : mb_substr($v, 0, $max);
}
private static function arrayText(string $key, int $i, int $max): ?string
{
$v = trim((string) (($_POST[$key] ?? [])[$i] ?? ''));
return $v === '' ? null : mb_substr($v, 0, $max);
}
private static function numberOrNull(mixed $v): ?float
{
$v = str_replace(',', '.', trim((string) $v));
return $v !== '' && is_numeric($v) ? (float) $v : null;
}
private static function directory(int $animalId): string
{
return dirname(__DIR__, 2) . '/data/medical-documents/' . $animalId;
}
private static function storeUpload(string $key, int $animalId, string $prefix, bool $required): ?array
{
$f = $_FILES[$key] ?? null;
if (!$f || ($f['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) {
if ($required) {
throw new RuntimeException(t('medical_document.file_required'));
}
return null;
}
if (($f['error'] ?? 1) !== UPLOAD_ERR_OK || ($f['size'] ?? 0) <= 0 || $f['size'] > self::MAX_BYTES) {
throw new RuntimeException(t('medical_document.file_invalid'));
}
$mime = new finfo(FILEINFO_MIME_TYPE)->file($f['tmp_name']);
if (!isset(self::TYPES[$mime])) {
throw new RuntimeException(t('medical_document.file_type'));
}
$dir = self::directory($animalId);
if (!is_dir($dir) && !mkdir($dir, 0770, true) && !is_dir($dir)) {
throw new RuntimeException(t('medical_document.storage_failed'));
}
$stored = $prefix . '-' . bin2hex(random_bytes(16)) . '.' . self::TYPES[$mime];
if (!move_uploaded_file($f['tmp_name'], $dir . '/' . $stored)) {
throw new RuntimeException(t('medical_document.storage_failed'));
}
return [
'original' => mb_substr(basename((string) $f['name']), 0, 240),
'stored' => $stored,
'mime' => $mime,
'size' => (int) $f['size'],
];
}
private static function remove(int $animalId, string $name): void
{
@unlink(self::directory($animalId) . '/' . basename($name));
}
private static function history(PDO $db, int $animalId, string $label, string $details): void
{
$db->prepare(
"INSERT INTO animal_history(animal_id,type,label,details,user_id) VALUES(?,'medical',?,?,?)",
)->execute([$animalId, $label, $details, Auth::id()]);
}
private static function fr(string $date): string
{
return new DateTimeImmutable($date)->format('d/m/Y');
}
private static function fail(string $message): void
{
http_response_code(400);
echo h($message);
}
private static function back(int $id): never
{
header('Location: /animal?id=' . $id . '#tab-med');
exit();
}
}