Configure les tarifs d'adoption par espèce

Refs #14 #18
This commit is contained in:
Alexandre NOEL 2026-09-08 00:44:40 +02:00
commit 1378aefa9c
14 changed files with 93 additions and 35 deletions

View file

@ -495,6 +495,7 @@ final class SettingsController
->query('SELECT id,name FROM ref_dewormers ORDER BY name COLLATE NOCASE')
->fetchAll(PDO::FETCH_ASSOC),
'settings' => AppSettings::all(),
'adoptionFees' => PricingService::adoptionFees(),
'saved' => isset($_GET['saved']),
'error' => (string) ($_GET['error'] ?? ''),
]);
@ -616,19 +617,23 @@ final class SettingsController
{
self::admin();
self::postOnly();
AppSettings::save(
[
'adoption_fee_sterilized' => (string) max(
0,
min(2000, (int) ($_POST['adoption_fee_sterilized'] ?? 220)),
),
'adoption_fee_unsterilized' => (string) max(
0,
min(2000, (int) ($_POST['adoption_fee_unsterilized'] ?? 200)),
),
],
Auth::id(),
$submitted = (array) ($_POST['fees'] ?? []);
$allowed = array_keys(PricingService::adoptionFees());
$stmt = DB::pdo()->prepare(
"INSERT INTO adoption_fee_rules(species_code,fee_before_sterilization,fee_after_sterilization,is_free,updated_at) VALUES(?,?,?,?,datetime('now')) ON CONFLICT(species_code) DO UPDATE SET fee_before_sterilization=excluded.fee_before_sterilization,fee_after_sterilization=excluded.fee_after_sterilization,is_free=excluded.is_free,updated_at=datetime('now')",
);
foreach ($submitted as $species => $values) {
$species = SpeciesService::normalize((string) $species);
if (!in_array($species, $allowed, true) || !is_array($values)) {
continue;
}
$stmt->execute([
$species,
max(0, min(2000, (int) ($values['before'] ?? 0))),
max(0, min(2000, (int) ($values['after'] ?? 0))),
isset($values['free']) ? 1 : 0,
]);
}
AuditService::log('adoption_fees_saved', '/settings/pricing/adoption', 'Tarifs d’adoption modifiés');
header('Location: /settings/pricing?saved=1');
exit();

View file

@ -37,6 +37,23 @@ final class PricingService
{
return max(0, (int) round((float) str_replace(',', '.', trim($v)) * 100));
}
public static function adoptionFees(): array
{
$rows = DB::pdo()->query(
"SELECT s.code,s.name,s.icon,COALESCE(r.fee_before_sterilization,200) fee_before_sterilization,COALESCE(r.fee_after_sterilization,220) fee_after_sterilization,COALESCE(r.is_free,0) is_free FROM ref_species s LEFT JOIN adoption_fee_rules r ON r.species_code=s.code WHERE s.active=1 ORDER BY s.sort_order,s.name COLLATE NOCASE",
)->fetchAll(PDO::FETCH_ASSOC);
return array_column($rows, null, 'code');
}
public static function adoptionFee(string $species, bool $sterilized): ?int
{
$fees = self::adoptionFees();
$rule = $fees[SpeciesService::normalize($species)] ?? null;
if (!$rule || (int) $rule['is_free'] === 1) {
return null;
}
return (int) $rule[$sterilized ? 'fee_after_sterilization' : 'fee_before_sterilization'];
}
public static function tariffs(bool $active = false): array
{
$w = $active ? 'WHERE ct.active=1 AND dc.deleted_at IS NULL' : '';

View file

@ -641,7 +641,7 @@ $today = date('Y-m-d');
<span class="form-check-label">
<span class="fw-bold d-block"><?= h(t('animal.sterilization_act')) ?></span>
<span
class="form-hint mt-0"><?= h(!empty($animal['sterilized']) ? t('animal.already_sterilized') : t('animal.sterilization_auto', ['price' => AppSettings::int('adoption_fee_sterilized')])) ?></span>
class="form-hint mt-0"><?php $feeAfterSterilization = PricingService::adoptionFee((string)$animal['species'], true); ?><?= h(!empty($animal['sterilized']) ? t('animal.already_sterilized') : ($feeAfterSterilization === null ? t('animal.sterilization_auto_free') : t('animal.sterilization_auto', ['price' => $feeAfterSterilization]))) ?></span>
</span>
</label>
</div>

View file

@ -285,9 +285,9 @@ if ($felvPos) {
<span class="badge <?= h($sterilizationClass) ?>">
<?= h($sterilizationLabel) ?>
</span>
<?php if ($isCat): ?><span class="text-muted small"><?= h(t('animal.adoption_fee_hint')) ?>
<strong
class="text-body"><?= $sterilizationState === 'yes' ? AppSettings::int('adoption_fee_sterilized') . ' €' : AppSettings::int('adoption_fee_unsterilized') . ' €' ?></strong></span><?php endif; ?>
<?php $adoptionFee = PricingService::adoptionFee((string)$animal['species'], $sterilizationState === 'yes'); ?>
<span class="text-muted small"><?= h(t('animal.adoption_fee_hint')) ?>
<strong class="text-body"><?= $adoptionFee === null ? h(t('pricing.free_fee')) : $adoptionFee . ' €' ?></strong></span>
<?php if ($sterilizationState === 'yes' && (!empty($animal['sterilization_date']) || !empty($sterilizationContacts))): ?><span
class="text-muted small"><?= !empty($animal['sterilization_date']) ? h(date('d/m/Y', strtotime((string)$animal['sterilization_date']))) : '' ?><?= !empty($sterilizationContacts['vet']['name']) ? ' · ' . h($sterilizationContacts['vet']['name']) : '' ?><?= !empty($sterilizationContacts['clinic']['name']) ? ' · ' . h($sterilizationContacts['clinic']['name']) : '' ?></span><?php endif; ?>
</div>

View file

@ -31,9 +31,9 @@
<div class="col-lg-6">
<section class="card h-100">
<div class="card-body">
<span class="badge bg-blue-lt mb-2">1.0.0-rc.11</span>
<h3>Salles et tournées configurables</h3>
<p class="text-muted">Critères d’accueil par salle et créneaux de soins adaptés à l’organisation du refuge.</p>
<span class="badge bg-blue-lt mb-2">1.0.0-rc.12</span>
<h3>Tarifs d’adoption par espèce</h3>
<p class="text-muted">Montants avant ou après stérilisation, avec possibilité de laisser le tarif libre.</p>
</div>
</section>
</div>

View file

@ -6,7 +6,7 @@
<?php if ($saved): ?><div class="alert alert-success">✅ <?= h(t('pricing.saved')) ?></div>
<?php endif; ?><?php if ($error !== ''): ?><div class="alert alert-danger"><?= h($error) ?></div><?php endif; ?>
<div class="row row-cards mb-4">
<div class="col-lg-5">
<div class="col-lg-7">
<form class="card h-100" method="post" action="/settings/pricing/adoption">
<div class="card-status-top bg-green"></div>
<div class="card-header">
@ -15,24 +15,19 @@
<div class="text-muted small"><?= h(t('pricing.adoption_fees_help')) ?></div>
</div>
</div>
<div class="card-body row g-3">
<div class="col-6"><label class="form-label"><?= h(t('pricing.sterilized')) ?></label>
<div class="input-group"><input class="form-control" type="number" min="0"
name="adoption_fee_sterilized"
value="<?= (int)$settings['adoption_fee_sterilized'] ?>"><span
class="input-group-text">€</span></div>
</div>
<div class="col-6"><label class="form-label"><?= h(t('pricing.unsterilized')) ?></label>
<div class="input-group"><input class="form-control" type="number" min="0"
name="adoption_fee_unsterilized"
value="<?= (int)$settings['adoption_fee_unsterilized'] ?>"><span
class="input-group-text">€</span></div>
</div>
<div class="table-responsive">
<table class="table table-vcenter mb-0"><thead><tr><th><?= h(t('animal.species')) ?></th><th><?= h(t('pricing.before_sterilization')) ?></th><th><?= h(t('pricing.after_sterilization')) ?></th><th><?= h(t('pricing.free_fee')) ?></th></tr></thead><tbody>
<?php foreach ($adoptionFees as $species => $fee): ?><tr><td class="fw-semibold"><?= h(($fee['icon'] ?? '') . ' ' . $fee['name']) ?></td>
<td><div class="input-group"><input class="form-control" type="number" min="0" max="2000" name="fees[<?= h($species) ?>][before]" value="<?= (int)$fee['fee_before_sterilization'] ?>"><span class="input-group-text">€</span></div></td>
<td><div class="input-group"><input class="form-control" type="number" min="0" max="2000" name="fees[<?= h($species) ?>][after]" value="<?= (int)$fee['fee_after_sterilization'] ?>"><span class="input-group-text">€</span></div></td>
<td><label class="form-check form-switch"><input class="form-check-input adoption-fee-free" type="checkbox" name="fees[<?= h($species) ?>][free]" <?= (int)$fee['is_free'] === 1 ? 'checked' : '' ?>><span class="form-check-label"><?= h(t('pricing.free')) ?></span></label></td>
</tr><?php endforeach; ?>
</tbody></table>
</div>
<div class="card-footer text-end"><button class="btn btn-primary"><?= h(t('common.save')) ?></button></div>
</form>
</div>
<div class="col-lg-7">
<div class="col-lg-5">
<div class="card h-100">
<div class="card-status-top bg-orange"></div>
<div class="card-header">