t('role.adoptant'), 'benevole' => t('role.benevole'), 'cabinet' => t('role.cabinet'), 'crematorium' => t('role.crematorium'), 'deposant' => t('role.deposant'), 'dirigeant' => t('role.dirigeant'), 'fa' => t('role.fa'), 'fourriere' => t('role.fourriere'), 'veterinaire' => t('role.veterinaire'), ]; } public static function index(): void { $db = DB::pdo(); $q = trim((string) ($_GET['q'] ?? '')); $role = trim((string) ($_GET['role'] ?? '')); $where = ['dc.deleted_at IS NULL']; $params = []; if ($q !== '') { $where[] = '(dc.name LIKE :q OR dc.phone LIKE :q OR dc.email LIKE :q OR dc.city LIKE :q)'; $params[':q'] = '%' . $q . '%'; } $roles = self::roles(); if (isset($roles[$role])) { $where[] = 'EXISTS(SELECT 1 FROM directory_contact_roles f WHERE f.contact_id=dc.id AND f.role=:role)'; $params[':role'] = $role; } $stmt = $db->prepare( " SELECT dc.*, org.name AS organization_name, group_concat(dcr.role, ',') AS roles, (SELECT COUNT(*) FROM ( SELECT animal_id FROM adoptions ad WHERE ad.adopter_contact_id=dc.id OR lower(trim(ad.adopter_name))=lower(trim(dc.name)) UNION SELECT animal_id FROM animal_placements ap WHERE ap.contact_id=dc.id AND ap.event_type='adoption' UNION SELECT animal_id FROM animal_movements am WHERE am.kind='exit' AND lower(trim(am.contact_name))=lower(trim(dc.name)) AND lower(COALESCE(am.lieu,'')) LIKE 'adopt%' )) AS adoption_count, (SELECT COALESCE(SUM(ae.total_cents),0) FROM animal_expenses ae WHERE ae.clinic_contact_id=dc.id) AS expense_total_cents FROM directory_contacts dc LEFT JOIN directory_contacts org ON org.id=dc.organization_id LEFT JOIN directory_contact_roles dcr ON dcr.contact_id=dc.id WHERE " . implode(' AND ', $where) . ' GROUP BY dc.id ORDER BY dc.name COLLATE NOCASE ', ); $stmt->execute($params); render('directory_list.php', [ 'title' => t('directory.title'), 'contacts' => $stmt->fetchAll(), 'roles' => $roles, 'q' => $q, 'role' => $role, ]); } public static function show(): void { $db = DB::pdo(); $id = (int) ($_GET['id'] ?? 0); $stmt = $db->prepare( "SELECT dc.*,org.name organization_name,group_concat(r.role,',') roles FROM directory_contacts dc LEFT JOIN directory_contacts org ON org.id=dc.organization_id LEFT JOIN directory_contact_roles r ON r.contact_id=dc.id WHERE dc.id=:id AND dc.deleted_at IS NULL GROUP BY dc.id", ); $stmt->execute([':id' => $id]); $contact = $stmt->fetch(PDO::FETCH_ASSOC); if (!$contact) { http_response_code(404); echo h(t('directory.not_found')); return; } $adoptions = $db->prepare("SELECT a.id,a.name,a.internal_code,a.species,a.status,x.event_date FROM animals a JOIN (SELECT animal_id,MAX(event_date) event_date FROM ( SELECT animal_id,adoption_date event_date FROM adoptions WHERE adopter_contact_id=:id1 OR lower(trim(adopter_name))=lower(trim(:name1)) UNION ALL SELECT animal_id,event_date FROM animal_placements WHERE contact_id=:id2 AND event_type='adoption' UNION ALL SELECT animal_id,created_at event_date FROM animal_movements WHERE kind='exit' AND lower(trim(contact_name))=lower(trim(:name2)) AND lower(COALESCE(lieu,'')) LIKE 'adopt%' ) GROUP BY animal_id) x ON x.animal_id=a.id ORDER BY date(x.event_date) DESC,a.name COLLATE NOCASE"); $adoptions->execute([':id1' => $id, ':name1' => $contact['name'], ':id2' => $id, ':name2' => $contact['name']]); $fosters = $db->prepare( "SELECT a.id,a.name,a.internal_code,a.species,a.status,MAX(ap.event_date) event_date FROM animal_placements ap JOIN animals a ON a.id=ap.animal_id WHERE ap.contact_id=:id AND ap.event_type='foster' GROUP BY a.id ORDER BY date(event_date) DESC,a.name COLLATE NOCASE", ); $fosters->execute([':id' => $id]); $deposited = $db->prepare( 'SELECT id,name,internal_code,species,status,intake_date event_date FROM animals WHERE depositor_contact_id=:id ORDER BY date(intake_date) DESC,name COLLATE NOCASE', ); $deposited->execute([':id' => $id]); $periodRequested = array_key_exists('year', $_GET); $periodRaw = (string) ($_GET['year'] ?? 'all'); $periodYear = ctype_digit($periodRaw) && (int) $periodRaw >= 2000 && (int) $periodRaw <= 2100 ? (int) $periodRaw : null; $periodMunicipality = trim((string) ($_GET['municipality'] ?? '')); $expenseWhere = ['ae.clinic_contact_id=:id', 'a.deleted_at IS NULL']; $expenseParams = [':id' => $id]; if ($periodYear !== null) { $expenseWhere[] = 'date(ae.occurred_on) BETWEEN :expense_start AND :expense_end'; $expenseParams[':expense_start'] = sprintf('%04d-01-01', $periodYear); $expenseParams[':expense_end'] = sprintf('%04d-12-31', $periodYear); } $expenses = $db->prepare( 'SELECT ae.*,a.name animal_name,a.internal_code,a.rescue_location_name,a.rescue_address FROM animal_expenses ae JOIN animals a ON a.id=ae.animal_id WHERE ' . implode(' AND ', $expenseWhere) . ' ORDER BY date(ae.occurred_on) DESC,ae.id DESC', ); $expenses->execute($expenseParams); $expenseRows = $expenses->fetchAll(PDO::FETCH_ASSOC); if ($periodMunicipality !== '') { $expenseRows = array_values( array_filter( $expenseRows, static fn($row) => GrantService::municipality( (string) ($row['rescue_location_name'] ?: $row['rescue_address']), ) === $periodMunicipality, ), ); } $expenseTotal = array_sum(array_map(static fn($row) => (int) $row['total_cents'], $expenseRows)); $expenseCount = count($expenseRows); $expenseRows = array_slice($expenseRows, 0, 250); $invoiceWhere = ['vendor_contact_id=:vendor', 'deleted_at IS NULL']; $invoiceParams = [':vendor' => $id]; if ($periodYear !== null) { $invoiceWhere[] = "strftime('%Y',invoice_date)=:invoice_year"; $invoiceParams[':invoice_year'] = (string) $periodYear; } $invoiceStmt = $db->prepare( 'SELECT * FROM accounting_invoices WHERE ' . implode(' AND ', $invoiceWhere) . ' ORDER BY date(invoice_date) DESC,id DESC', ); $invoiceStmt->execute($invoiceParams); $contactInvoices = $invoiceStmt->fetchAll(PDO::FETCH_ASSOC); $contactInvoiceTotal = array_sum(array_column($contactInvoices, 'total_ttc_cents')); $contactInvoiceDue = array_sum(array_column($contactInvoices, 'amount_due_cents')); $linkedPeople = []; if ($contact['kind'] === 'organization') { $s = $db->prepare( "SELECT dc.id,dc.name,dc.phone,dc.email,group_concat(r.role,',') roles FROM directory_contacts dc LEFT JOIN directory_contact_roles r ON r.contact_id=dc.id WHERE dc.organization_id=:id AND dc.deleted_at IS NULL GROUP BY dc.id ORDER BY dc.name COLLATE NOCASE", ); $s->execute([':id' => $id]); $linkedPeople = $s->fetchAll(PDO::FETCH_ASSOC); } render('directory_show_accounting.php', [ 'title' => $contact['name'], 'contact' => $contact, 'roles' => self::roles(), 'adoptions' => $adoptions->fetchAll(PDO::FETCH_ASSOC), 'fosters' => $fosters->fetchAll(PDO::FETCH_ASSOC), 'deposited' => $deposited->fetchAll(PDO::FETCH_ASSOC), 'expenses' => $expenseRows, 'expenseTotal' => $expenseTotal, 'expenseCount' => $expenseCount, 'expensePeriodRequested' => $periodRequested, 'expensePeriodYear' => $periodYear, 'expensePeriodMunicipality' => $periodMunicipality, 'contactInvoices' => $contactInvoices, 'contactInvoiceTotal' => $contactInvoiceTotal, 'contactInvoiceDue' => $contactInvoiceDue, 'linkedPeople' => $linkedPeople, ]); } public static function form(): void { $db = DB::pdo(); $id = (int) ($_GET['id'] ?? 0); $contact = [ 'id' => 0, 'kind' => 'person', 'name' => '', 'phone' => '', 'email' => '', 'address' => '', 'postal_code' => '', 'city' => '', 'country' => 'France', 'organization_id' => '', 'notes' => '', ]; $selected = []; if ($id) { $stmt = $db->prepare('SELECT * FROM directory_contacts WHERE id=:id AND deleted_at IS NULL'); $stmt->execute([':id' => $id]); $contact = $stmt->fetch() ?: $contact; $stmt = $db->prepare('SELECT role FROM directory_contact_roles WHERE contact_id=:id'); $stmt->execute([':id' => $id]); $selected = $stmt->fetchAll(PDO::FETCH_COLUMN); } $organizations = $db ->query( "SELECT id,name FROM directory_contacts WHERE kind='organization' AND deleted_at IS NULL ORDER BY name COLLATE NOCASE", ) ->fetchAll(); render('directory_form.php', [ 'title' => $id ? t('directory.edit') : t('directory.add'), 'contact' => $contact, 'selectedRoles' => $selected, 'roles' => self::roles(), 'organizations' => $organizations, ]); } public static function save(): void { if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') { http_response_code(405); return; } $db = DB::pdo(); $id = (int) ($_POST['id'] ?? 0); $name = trim((string) ($_POST['name'] ?? '')); if ($name === '') { http_response_code(400); echo h(t('directory.name_required')); return; } $kind = ($_POST['kind'] ?? 'person') === 'organization' ? 'organization' : 'person'; $roles = array_values( array_intersect(array_keys(self::roles()), is_array($_POST['roles'] ?? null) ? $_POST['roles'] : []), ); $organizationId = $kind === 'person' ? ((int) ($_POST['organization_id'] ?? 0) ?: null) : null; if ($organizationId !== null) { $check = $db->prepare( "SELECT 1 FROM directory_contacts WHERE id=:id AND kind='organization' AND deleted_at IS NULL", ); $check->execute([':id' => $organizationId]); if (!$check->fetchColumn() || $organizationId === $id) { http_response_code(400); echo h(t('directory.invalid_organization')); return; } } $data = [ ':kind' => $kind, ':name' => $name, ':phone' => trim((string) ($_POST['phone'] ?? '')) ?: null, ':email' => trim((string) ($_POST['email'] ?? '')) ?: null, ':address' => trim((string) ($_POST['address'] ?? '')) ?: null, ':postal' => trim((string) ($_POST['postal_code'] ?? '')) ?: null, ':city' => trim((string) ($_POST['city'] ?? '')) ?: null, ':country' => trim((string) ($_POST['country'] ?? 'France')) ?: 'France', ':org' => $organizationId, ':notes' => trim((string) ($_POST['notes'] ?? '')) ?: null, ]; $db->beginTransaction(); try { if ($id) { $data[':id'] = $id; $db->prepare( "UPDATE directory_contacts SET kind=:kind,name=:name,phone=:phone,email=:email,address=:address,postal_code=:postal,city=:city,country=:country,organization_id=:org,notes=:notes,updated_at=datetime('now') WHERE id=:id", )->execute($data); } else { $db->prepare( 'INSERT INTO directory_contacts(kind,name,phone,email,address,postal_code,city,country,organization_id,notes) VALUES(:kind,:name,:phone,:email,:address,:postal,:city,:country,:org,:notes)', )->execute($data); $id = (int) $db->lastInsertId(); } $db->prepare('DELETE FROM directory_contact_roles WHERE contact_id=:id')->execute([':id' => $id]); $ins = $db->prepare('INSERT INTO directory_contact_roles(contact_id,role) VALUES(:id,:role)'); foreach ($roles as $r) { $ins->execute([':id' => $id, ':role' => $r]); } $db->commit(); header('Location: /directory'); exit(); } catch (Throwable $e) { if ($db->inTransaction()) { $db->rollBack(); } throw $e; } } }