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

174
migrations/001_init.sql Normal file
View file

@ -0,0 +1,174 @@
-- 001_init.sql
PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS _migrations (
name TEXT PRIMARY KEY,
applied_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'admin',
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- Référentiels
CREATE TABLE IF NOT EXISTS ref_medications (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE, -- ex: Doxybactin
molecule TEXT, -- ex: doxycycline
form TEXT, -- ex: cp / sirop / inj
notes TEXT
);
CREATE TABLE IF NOT EXISTS ref_vaccines (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE, -- ex: TCL / Rage
notes TEXT
);
CREATE TABLE IF NOT EXISTS animals (
id INTEGER PRIMARY KEY AUTOINCREMENT,
internal_code TEXT NOT NULL UNIQUE, -- ex: 2026-001-CHOCOLAT
name TEXT NOT NULL,
species TEXT NOT NULL DEFAULT 'chat',
sex TEXT CHECK (sex IN ('M','F','U')) NOT NULL DEFAULT 'U',
birth_date TEXT, -- YYYY-MM-DD
chip_id TEXT, -- puce
status TEXT NOT NULL DEFAULT 'refuge', -- refuge / fa / reserve / adopte / fugue / decede
fiv INTEGER NOT NULL DEFAULT 0,
felv INTEGER NOT NULL DEFAULT 0,
sterilized INTEGER NOT NULL DEFAULT 0,
color TEXT,
notes TEXT,
current_address TEXT,
current_lat REAL,
current_lng REAL,
rescue_address TEXT,
rescue_lat REAL,
rescue_lng REAL,
rescue_location_name TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_animals_status ON animals(status);
CREATE INDEX IF NOT EXISTS idx_animals_name ON animals(name);
CREATE INDEX IF NOT EXISTS idx_animals_code ON animals(internal_code);
CREATE TABLE IF NOT EXISTS adoptions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
animal_id INTEGER NOT NULL,
adopter_name TEXT NOT NULL,
adopter_phone TEXT,
adopter_email TEXT,
adoption_date DATE NOT NULL,
notes TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
adopter_address TEXT,
adopter_postal_code TEXT,
adopter_city TEXT,
adopter_country TEXT DEFAULT 'France',
FOREIGN KEY(animal_id) REFERENCES animals(id)
);
CREATE TABLE IF NOT EXISTS animal_movements (
id INTEGER PRIMARY KEY AUTOINCREMENT,
animal_id INTEGER NOT NULL,
kind TEXT NOT NULL CHECK(kind IN ('entry','exit')),
place TEXT,
lieu TEXT,
contact_name TEXT,
contact_phone TEXT,
contact_email TEXT,
note TEXT,
created_at TEXT NOT NULL DEFAULT(datetime('now')),
FOREIGN KEY(animal_id) REFERENCES animals(id)
);
-- Mesures (poids/temp/etc.)
CREATE TABLE IF NOT EXISTS measurements (
id INTEGER PRIMARY KEY AUTOINCREMENT,
animal_id INTEGER NOT NULL,
measured_at TEXT NOT NULL DEFAULT (datetime('now')),
type TEXT NOT NULL, -- weight / temp / rr / hr ...
value REAL NOT NULL,
unit TEXT NOT NULL,
notes TEXT,
created_by INTEGER,
FOREIGN KEY(animal_id) REFERENCES animals(id) ON DELETE CASCADE,
FOREIGN KEY(created_by) REFERENCES users(id) ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS idx_measurements_animal_date ON measurements(animal_id, measured_at);
-- Notes médicales “style dossier patient”
CREATE TABLE IF NOT EXISTS medical_notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
animal_id INTEGER NOT NULL,
noted_at TEXT NOT NULL DEFAULT (datetime('now')),
kind TEXT NOT NULL, -- consult / controle / symptome / diagnostic / acte / hosp / autre
reason TEXT, -- motif
symptoms TEXT, -- symptômes
exam TEXT, -- examen clinique
diagnosis TEXT, -- diagnostic / hypothèses
plan TEXT, -- plan / conduite à tenir
temperature_c REAL, -- optionnel
weight_kg REAL, -- optionnel
created_by INTEGER,
FOREIGN KEY(animal_id) REFERENCES animals(id) ON DELETE CASCADE,
FOREIGN KEY(created_by) REFERENCES users(id) ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS idx_mednotes_animal_date ON medical_notes(animal_id, noted_at);
-- Traitements (référentiel médocs)
CREATE TABLE IF NOT EXISTS treatments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
animal_id INTEGER NOT NULL,
medication_id INTEGER NOT NULL,
route TEXT, -- PO/SC/IM/Topique...
dose_text TEXT NOT NULL, -- "1/2 cp matin/soir"
start_date TEXT NOT NULL, -- YYYY-MM-DD
end_date TEXT, -- YYYY-MM-DD
ongoing INTEGER NOT NULL DEFAULT 1,
notes TEXT,
created_by INTEGER,
FOREIGN KEY(animal_id) REFERENCES animals(id) ON DELETE CASCADE,
FOREIGN KEY(medication_id) REFERENCES ref_medications(id) ON DELETE RESTRICT,
FOREIGN KEY(created_by) REFERENCES users(id) ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS idx_treatments_animal_ongoing ON treatments(animal_id, ongoing);
-- Vaccins
CREATE TABLE IF NOT EXISTS vaccinations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
animal_id INTEGER NOT NULL,
vaccine_id INTEGER NOT NULL,
done_date TEXT NOT NULL, -- YYYY-MM-DD
due_date TEXT, -- rappel
lot TEXT,
notes TEXT,
created_by INTEGER,
FOREIGN KEY(animal_id) REFERENCES animals(id) ON DELETE CASCADE,
FOREIGN KEY(vaccine_id) REFERENCES ref_vaccines(id) ON DELETE RESTRICT,
FOREIGN KEY(created_by) REFERENCES users(id) ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS idx_vaccinations_due ON vaccinations(due_date);
-- Vue résumé médical (liste animaux)
CREATE VIEW IF NOT EXISTS v_animals_med_summary AS
SELECT
a.id AS animal_id,
(SELECT mn.noted_at
FROM medical_notes mn
WHERE mn.animal_id = a.id
ORDER BY mn.noted_at DESC
LIMIT 1) AS last_med_at,
(SELECT COUNT(*)
FROM treatments t
WHERE t.animal_id = a.id AND t.ongoing = 1) AS ongoing_treatments,
(SELECT MIN(v.due_date)
FROM vaccinations v
WHERE v.animal_id = a.id AND v.due_date IS NOT NULL AND date(v.due_date) >= date('now')) AS next_vaccine_due
FROM animals a;

View file

@ -0,0 +1,5 @@
ALTER TABLE ref_vaccines ADD COLUMN family TEXT;
-- Mapping simple (à ajuster si tu as d'autres libellés)
UPDATE ref_vaccines SET family='TC' WHERE upper(name) LIKE '%TC%' OR upper(name) LIKE '%TCL%' OR upper(name) LIKE '%CORYZA%' OR upper(name) LIKE '%TYPHUS%';
UPDATE ref_vaccines SET family='L' WHERE upper(name) LIKE '%LEUCO%' OR upper(name) LIKE '%FELV%' OR upper(name) = 'L';

View file

@ -0,0 +1,8 @@
CREATE TABLE IF NOT EXISTS clinics (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE
);
-- Exemples (tu adapteras)
INSERT OR IGNORE INTO clinics(name) VALUES ('Clinique A');
INSERT OR IGNORE INTO clinics(name) VALUES ('Clinique B');

View file

@ -0,0 +1,8 @@
ALTER TABLE medical_notes ADD COLUMN clinic_id INTEGER;
ALTER TABLE medical_notes ADD COLUMN vet_name TEXT;
ALTER TABLE treatments ADD COLUMN clinic_id INTEGER;
ALTER TABLE treatments ADD COLUMN vet_name TEXT;
ALTER TABLE vaccinations ADD COLUMN clinic_id INTEGER;
ALTER TABLE vaccinations ADD COLUMN vet_name TEXT;

View file

@ -0,0 +1,16 @@
CREATE VIEW IF NOT EXISTS v_animals_weight_trend AS
SELECT
a.id AS animal_id,
(SELECT m.value
FROM measurements m
WHERE m.animal_id = a.id AND m.type = 'weight'
ORDER BY datetime(m.measured_at) DESC
LIMIT 1) AS last_weight,
(SELECT m.value
FROM measurements m
WHERE m.animal_id = a.id AND m.type = 'weight'
ORDER BY datetime(m.measured_at) DESC
LIMIT 1 OFFSET 1) AS prev_weight
FROM animals a;

View file

@ -0,0 +1,18 @@
DROP VIEW IF EXISTS v_animals_weight_trend;
CREATE VIEW v_animals_weight_trend AS
SELECT
a.id AS animal_id,
(SELECT m.value
FROM measurements m
WHERE m.animal_id = a.id AND m.type = 'weight'
ORDER BY datetime(m.measured_at) DESC, m.id DESC
LIMIT 1) AS last_weight,
(SELECT m.value
FROM measurements m
WHERE m.animal_id = a.id AND m.type = 'weight'
ORDER BY datetime(m.measured_at) DESC, m.id DESC
LIMIT 1 OFFSET 1) AS prev_weight
FROM animals a;

View file

@ -0,0 +1,7 @@
ALTER TABLE animals ADD COLUMN fiv_status TEXT;
ALTER TABLE animals ADD COLUMN felv_status TEXT;
UPDATE animals
SET fiv_status = CASE WHEN fiv = 1 THEN 'pos' ELSE 'neg' END,
felv_status = CASE WHEN felv = 1 THEN 'pos' ELSE 'neg' END
WHERE fiv_status IS NULL AND felv_status IS NULL;

View file

@ -0,0 +1,26 @@
CREATE TABLE IF NOT EXISTS animal_photos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
animal_id INTEGER NOT NULL,
filename TEXT NOT NULL,
original_name TEXT,
mime TEXT,
size_bytes INTEGER,
is_primary INTEGER DEFAULT 0,
is_public INTEGER NOT NULL DEFAULT 1,
created_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_animal_photos_animal ON animal_photos(animal_id);
CREATE TABLE IF NOT EXISTS medical_photos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
animal_id INTEGER NOT NULL,
medical_note_id INTEGER NOT NULL,
filename TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY(animal_id) REFERENCES animals(id),
FOREIGN KEY(medical_note_id) REFERENCES medical_notes(id)
);
CREATE INDEX IF NOT EXISTS idx_medical_photos_animal ON medical_photos(animal_id);
CREATE INDEX IF NOT EXISTS idx_medical_photos_note ON medical_photos(medical_note_id);

View file

@ -0,0 +1,30 @@
DROP VIEW IF EXISTS v_animals_weight_trend;
CREATE VIEW v_animals_weight_trend AS
SELECT
a.id AS animal_id,
(
SELECT m.value
FROM measurements m
WHERE m.animal_id = a.id AND m.type='weight'
ORDER BY m.id DESC
LIMIT 1
) AS last_weight,
(
SELECT m2.value
FROM measurements m2
WHERE m2.animal_id = a.id
AND m2.type='weight'
AND m2.value != (
SELECT m3.value
FROM measurements m3
WHERE m3.animal_id = a.id AND m3.type='weight'
ORDER BY m3.id DESC
LIMIT 1
)
ORDER BY m2.id DESC
LIMIT 1
) AS prev_weight
FROM animals a;

View file

@ -0,0 +1,2 @@
ALTER TABLE animals ADD COLUMN intake_date TEXT;
ALTER TABLE animals ADD COLUMN quarantine_until TEXT;

View file

@ -0,0 +1,34 @@
DROP VIEW IF EXISTS v_animals_weight_alerts;
CREATE VIEW v_animals_weight_alerts AS
SELECT
a.id AS animal_id,
-- dernier poids
(
SELECT m.value
FROM measurements m
WHERE m.animal_id=a.id AND m.type='weight'
ORDER BY m.id DESC
LIMIT 1
) AS last_weight,
-- poids de référence ~30 jours avant : le plus récent <= J-30
(
SELECT m.value
FROM measurements m
WHERE m.animal_id=a.id AND m.type='weight'
AND date(m.measured_at) <= date('now','-30 day')
ORDER BY date(m.measured_at) DESC, m.id DESC
LIMIT 1
) AS w30_weight,
(
SELECT m.measured_at
FROM measurements m
WHERE m.animal_id=a.id AND m.type='weight'
AND date(m.measured_at) <= date('now','-30 day')
ORDER BY date(m.measured_at) DESC, m.id DESC
LIMIT 1
) AS w30_date
FROM animals a;

View file

@ -0,0 +1 @@
ALTER TABLE animals ADD COLUMN chip TEXT;

View file

@ -0,0 +1,4 @@
ALTER TABLE animals ADD COLUMN breed TEXT DEFAULT 'Chat Européen';
ALTER TABLE animals ADD COLUMN birth_is_estimated INTEGER DEFAULT 0;
ALTER TABLE animals ADD COLUMN birth_estimated_months INTEGER;

View file

@ -0,0 +1,2 @@
-- Conservé pour la compatibilité historique : ces colonnes sont déjà créées par 004.
SELECT 1;

View file

@ -0,0 +1,2 @@
-- Conservé pour la compatibilité historique : ces colonnes sont déjà créées par 004.
SELECT 1;

View file

@ -0,0 +1,12 @@
CREATE TABLE IF NOT EXISTS animal_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
animal_id INTEGER NOT NULL,
action TEXT NOT NULL, -- created, updated, status_changed, note_added...
field TEXT, -- name, status, etc (optionnel)
old_value TEXT,
new_value TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
meta TEXT -- JSON léger optionnel (ex: {"route":"..."} )
);
CREATE INDEX IF NOT EXISTS idx_animal_history_animal ON animal_history(animal_id, created_at DESC);

View file

@ -0,0 +1 @@
ALTER TABLE animals ADD COLUMN hair_type TEXT;

View file

@ -0,0 +1,3 @@
UPDATE animals
SET species = 'chat'
WHERE lower(trim(species)) = 'cat';

View file

@ -0,0 +1,15 @@
CREATE TABLE IF NOT EXISTS animal_health_conditions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
animal_id INTEGER NOT NULL,
name TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'resolved')),
diagnosed_at TEXT,
resolved_at TEXT,
notes TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY(animal_id) REFERENCES animals(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_health_conditions_animal_status
ON animal_health_conditions(animal_id, status);

View file

@ -0,0 +1,26 @@
PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS litters (
id INTEGER PRIMARY KEY AUTOINCREMENT,
mother_id INTEGER,
father_id INTEGER,
birth_date TEXT,
notes TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY(mother_id) REFERENCES animals(id) ON DELETE SET NULL,
FOREIGN KEY(father_id) REFERENCES animals(id) ON DELETE SET NULL,
CHECK (mother_id IS NULL OR father_id IS NULL OR mother_id != father_id)
);
CREATE TABLE IF NOT EXISTS litter_kittens (
litter_id INTEGER NOT NULL,
animal_id INTEGER NOT NULL UNIQUE,
position INTEGER NOT NULL DEFAULT 1,
PRIMARY KEY(litter_id, animal_id),
FOREIGN KEY(litter_id) REFERENCES litters(id) ON DELETE CASCADE,
FOREIGN KEY(animal_id) REFERENCES animals(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_litters_mother ON litters(mother_id);
CREATE INDEX IF NOT EXISTS idx_litters_father ON litters(father_id);
CREATE INDEX IF NOT EXISTS idx_litter_kittens_litter ON litter_kittens(litter_id, position);

View file

@ -0,0 +1,9 @@
ALTER TABLE animals ADD COLUMN archived_at TEXT;
ALTER TABLE animals ADD COLUMN deletion_reason TEXT;
-- L'ancien bouton « Archiver » utilisait deleted_at : on conserve donc ces fiches
-- comme archives et on libère la nouvelle corbeille.
UPDATE animals SET archived_at = deleted_at, deleted_at = NULL WHERE deleted_at IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_animals_archived_at ON animals(archived_at);
CREATE INDEX IF NOT EXISTS idx_animals_deleted_at ON animals(deleted_at);

View file

@ -0,0 +1,2 @@
ALTER TABLE animals ADD COLUMN refuge_room TEXT CHECK (refuge_room IN ('silver', 'twix') OR refuge_room IS NULL);
CREATE INDEX IF NOT EXISTS idx_animals_refuge_room ON animals(refuge_room);

View file

@ -0,0 +1,23 @@
PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS bonded_groups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
notes TEXT,
active INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
dissolved_at TEXT
);
CREATE TABLE IF NOT EXISTS bonded_group_members (
group_id INTEGER NOT NULL,
animal_id INTEGER NOT NULL,
position INTEGER NOT NULL DEFAULT 1,
PRIMARY KEY(group_id, animal_id),
FOREIGN KEY(group_id) REFERENCES bonded_groups(id) ON DELETE CASCADE,
FOREIGN KEY(animal_id) REFERENCES animals(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_bonded_members_animal
ON bonded_group_members(animal_id);
CREATE INDEX IF NOT EXISTS idx_bonded_members_group ON bonded_group_members(group_id, position);

View file

@ -0,0 +1,76 @@
PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS directory_contacts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT NOT NULL DEFAULT 'person' CHECK(kind IN ('person','organization')),
name TEXT NOT NULL,
phone TEXT,
email TEXT,
address TEXT,
postal_code TEXT,
city TEXT,
country TEXT NOT NULL DEFAULT 'France',
organization_id INTEGER,
notes TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
deleted_at TEXT,
FOREIGN KEY(organization_id) REFERENCES directory_contacts(id) ON DELETE SET NULL
);
CREATE TABLE IF NOT EXISTS directory_contact_roles (
contact_id INTEGER NOT NULL,
role TEXT NOT NULL CHECK(role IN ('fa','benevole','dirigeant','veterinaire','cabinet','crematorium','adoptant')),
PRIMARY KEY(contact_id, role),
FOREIGN KEY(contact_id) REFERENCES directory_contacts(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_directory_name ON directory_contacts(name);
CREATE INDEX IF NOT EXISTS idx_directory_role ON directory_contact_roles(role, contact_id);
ALTER TABLE medical_notes ADD COLUMN vet_contact_id INTEGER REFERENCES directory_contacts(id) ON DELETE SET NULL;
ALTER TABLE medical_notes ADD COLUMN clinic_contact_id INTEGER REFERENCES directory_contacts(id) ON DELETE SET NULL;
ALTER TABLE treatments ADD COLUMN vet_contact_id INTEGER REFERENCES directory_contacts(id) ON DELETE SET NULL;
ALTER TABLE treatments ADD COLUMN clinic_contact_id INTEGER REFERENCES directory_contacts(id) ON DELETE SET NULL;
ALTER TABLE vaccinations ADD COLUMN vet_contact_id INTEGER REFERENCES directory_contacts(id) ON DELETE SET NULL;
ALTER TABLE vaccinations ADD COLUMN clinic_contact_id INTEGER REFERENCES directory_contacts(id) ON DELETE SET NULL;
ALTER TABLE adoptions ADD COLUMN adopter_contact_id INTEGER REFERENCES directory_contacts(id) ON DELETE SET NULL;
INSERT INTO directory_contacts(kind, name)
SELECT 'organization', c.name FROM clinics c
WHERE NOT EXISTS (SELECT 1 FROM directory_contacts dc WHERE dc.kind='organization' AND lower(dc.name)=lower(c.name));
INSERT OR IGNORE INTO directory_contact_roles(contact_id, role)
SELECT dc.id, 'cabinet' FROM directory_contacts dc JOIN clinics c ON lower(c.name)=lower(dc.name);
INSERT INTO directory_contacts(kind, name, phone, email, address, postal_code, city)
SELECT 'person', a.adopter_name, MAX(a.adopter_phone), MAX(a.adopter_email), MAX(a.adopter_address), MAX(a.adopter_postal_code), MAX(a.adopter_city)
FROM adoptions a
WHERE NOT EXISTS (
SELECT 1 FROM directory_contacts dc
WHERE dc.kind='person' AND lower(dc.name)=lower(a.adopter_name)
AND lower(COALESCE(dc.email,''))=lower(COALESCE(a.adopter_email,''))
)
GROUP BY lower(a.adopter_name), lower(COALESCE(a.adopter_email,''));
INSERT OR IGNORE INTO directory_contact_roles(contact_id, role)
SELECT DISTINCT dc.id, 'adoptant' FROM directory_contacts dc JOIN adoptions a
ON dc.kind='person' AND lower(dc.name)=lower(a.adopter_name)
AND lower(COALESCE(dc.email,''))=lower(COALESCE(a.adopter_email,''));
UPDATE adoptions SET adopter_contact_id = (
SELECT dc.id FROM directory_contacts dc
WHERE dc.kind='person' AND lower(dc.name)=lower(adoptions.adopter_name)
AND lower(COALESCE(dc.email,''))=lower(COALESCE(adoptions.adopter_email,'')) LIMIT 1
) WHERE adopter_contact_id IS NULL;
INSERT INTO directory_contacts(kind, name)
SELECT 'person', vet_name FROM (
SELECT vet_name FROM medical_notes UNION SELECT vet_name FROM treatments UNION SELECT vet_name FROM vaccinations
) vets
WHERE vet_name IS NOT NULL AND trim(vet_name)!=''
AND NOT EXISTS (SELECT 1 FROM directory_contacts dc WHERE dc.kind='person' AND lower(dc.name)=lower(vets.vet_name));
INSERT OR IGNORE INTO directory_contact_roles(contact_id, role)
SELECT dc.id, 'veterinaire' FROM directory_contacts dc
WHERE EXISTS (
SELECT 1 FROM medical_notes WHERE lower(vet_name)=lower(dc.name)
UNION SELECT 1 FROM treatments WHERE lower(vet_name)=lower(dc.name)
UNION SELECT 1 FROM vaccinations WHERE lower(vet_name)=lower(dc.name)
);

View file

@ -0,0 +1,57 @@
PRAGMA foreign_keys = ON;
ALTER TABLE animals ADD COLUMN care_box_key TEXT;
ALTER TABLE treatments ADD COLUMN give_morning INTEGER NOT NULL DEFAULT 0;
ALTER TABLE treatments ADD COLUMN give_evening INTEGER NOT NULL DEFAULT 0;
ALTER TABLE treatments ADD COLUMN give_as_needed INTEGER NOT NULL DEFAULT 0;
CREATE TABLE IF NOT EXISTS care_room_layouts (
room TEXT PRIMARY KEY CHECK(room IN ('care','quarantine')),
top_layout TEXT NOT NULL DEFAULT 'split' CHECK(top_layout IN ('split','merged')),
bottom_layout TEXT NOT NULL DEFAULT 'split' CHECK(bottom_layout IN ('split','left2','right2','merged')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
INSERT OR IGNORE INTO care_room_layouts(room) VALUES('care'),('quarantine');
CREATE TABLE IF NOT EXISTS care_rounds (
id INTEGER PRIMARY KEY AUTOINCREMENT,
round_date TEXT NOT NULL,
period TEXT NOT NULL CHECK(period IN ('morning','evening')),
performed_by TEXT,
started_at TEXT NOT NULL DEFAULT (datetime('now')),
completed_at TEXT,
UNIQUE(round_date, period)
);
CREATE TABLE IF NOT EXISTS care_round_observations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
round_id INTEGER NOT NULL,
animal_id INTEGER NOT NULL,
food TEXT NOT NULL DEFAULT 'normal' CHECK(food IN ('normal','little','refused')),
water TEXT NOT NULL DEFAULT 'normal' CHECK(water IN ('normal','renewed','abnormal')),
urine TEXT NOT NULL DEFAULT 'normal' CHECK(urine IN ('normal','absent','abnormal')),
stool TEXT NOT NULL DEFAULT 'normal' CHECK(stool IN ('normal','absent','abnormal')),
general_state TEXT NOT NULL DEFAULT 'good' CHECK(general_state IN ('good','watch','alert')),
comment TEXT,
checked_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE(round_id, animal_id),
FOREIGN KEY(round_id) REFERENCES care_rounds(id) ON DELETE CASCADE,
FOREIGN KEY(animal_id) REFERENCES animals(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS treatment_administrations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
round_id INTEGER NOT NULL,
treatment_id INTEGER NOT NULL,
animal_id INTEGER NOT NULL,
status TEXT NOT NULL CHECK(status IN ('given','skipped')),
comment TEXT,
administered_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE(round_id, treatment_id),
FOREIGN KEY(round_id) REFERENCES care_rounds(id) ON DELETE CASCADE,
FOREIGN KEY(treatment_id) REFERENCES treatments(id) ON DELETE CASCADE,
FOREIGN KEY(animal_id) REFERENCES animals(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_care_observations_round ON care_round_observations(round_id);
CREATE INDEX IF NOT EXISTS idx_treatment_admin_round ON treatment_administrations(round_id);

View file

@ -0,0 +1,28 @@
CREATE TABLE care_room_layouts_new (
room TEXT PRIMARY KEY CHECK(room IN ('care','quarantine')),
top_layout TEXT NOT NULL DEFAULT 'split' CHECK(top_layout IN ('split','left2','right2','merged')),
bottom_layout TEXT NOT NULL DEFAULT 'split' CHECK(bottom_layout IN ('split','merged')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
INSERT INTO care_room_layouts_new(room, top_layout, bottom_layout, updated_at)
SELECT room,
top_layout,
CASE WHEN bottom_layout = 'merged' THEN 'merged' ELSE 'split' END,
updated_at
FROM care_room_layouts;
DROP TABLE care_room_layouts;
ALTER TABLE care_room_layouts_new RENAME TO care_room_layouts;
-- Les anciennes positions sont rapprochées de leur nouvelle position physique.
-- Plusieurs chats pouvant partager un box, aucun animal n'est perdu lors du regroupement.
UPDATE animals
SET care_box_key = CASE care_box_key
WHEN 'bottom_b' THEN 'bottom_a'
WHEN 'bottom_c' THEN 'bottom_b'
WHEN 'bottom_ab' THEN 'bottom_a'
WHEN 'bottom_bc' THEN 'bottom_b'
ELSE care_box_key
END
WHERE care_box_key IN ('bottom_b','bottom_c','bottom_ab','bottom_bc');

View file

@ -0,0 +1 @@
ALTER TABLE care_round_observations ADD COLUMN alert_signature TEXT;

View file

@ -0,0 +1,2 @@
ALTER TABLE animal_photos ADD COLUMN caption TEXT;
ALTER TABLE animal_photos ADD COLUMN care_round_id INTEGER REFERENCES care_rounds(id) ON DELETE SET NULL;

View file

@ -0,0 +1,13 @@
CREATE TABLE directory_contact_roles_new (
contact_id INTEGER NOT NULL,
role TEXT NOT NULL CHECK(role IN ('fa','benevole','dirigeant','veterinaire','cabinet','crematorium','adoptant','deposant')),
PRIMARY KEY(contact_id, role),
FOREIGN KEY(contact_id) REFERENCES directory_contacts(id) ON DELETE CASCADE
);
INSERT INTO directory_contact_roles_new(contact_id,role)
SELECT contact_id,role FROM directory_contact_roles;
DROP TABLE directory_contact_roles;
ALTER TABLE directory_contact_roles_new RENAME TO directory_contact_roles;
CREATE INDEX idx_directory_role ON directory_contact_roles(role,contact_id);

View file

@ -0,0 +1,26 @@
ALTER TABLE users ADD COLUMN display_name TEXT;
ALTER TABLE users ADD COLUMN active INTEGER NOT NULL DEFAULT 1;
ALTER TABLE users ADD COLUMN last_login_at TEXT;
ALTER TABLE users ADD COLUMN updated_at TEXT NOT NULL DEFAULT (datetime('now'));
ALTER TABLE users ADD COLUMN failed_attempts INTEGER NOT NULL DEFAULT 0;
ALTER TABLE users ADD COLUMN locked_until TEXT;
ALTER TABLE animal_history ADD COLUMN user_id INTEGER REFERENCES users(id) ON DELETE SET NULL;
ALTER TABLE care_rounds ADD COLUMN performed_by_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL;
CREATE TABLE audit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
action TEXT NOT NULL,
method TEXT NOT NULL,
path TEXT NOT NULL,
entity_type TEXT,
entity_id INTEGER,
summary TEXT,
metadata TEXT,
ip_address TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE SET NULL
);
CREATE INDEX idx_audit_log_created ON audit_log(created_at DESC);
CREATE INDEX idx_audit_log_user ON audit_log(user_id,created_at DESC);

View file

@ -0,0 +1,29 @@
PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS animal_deaths (
animal_id INTEGER PRIMARY KEY,
deceased_date TEXT NOT NULL,
cause_code TEXT NOT NULL,
cause_details TEXT,
occurred_in_care INTEGER NOT NULL DEFAULT 1 CHECK(occurred_in_care IN (0,1)),
place_type TEXT NOT NULL DEFAULT 'refuge' CHECK(place_type IN ('refuge','fa','veterinaire','exterieur','inconnu','autre')),
place_details TEXT,
euthanized INTEGER NOT NULL DEFAULT 0 CHECK(euthanized IN (0,1)),
veterinarian_contact_id INTEGER,
crematorium_contact_id INTEGER,
body_disposition TEXT NOT NULL DEFAULT 'pending' CHECK(body_disposition IN ('pending','collective_cremation','individual_cremation','returned_family','burial','other')),
cremation_date TEXT,
notes TEXT,
created_by INTEGER,
updated_by INTEGER,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY(animal_id) REFERENCES animals(id) ON DELETE CASCADE,
FOREIGN KEY(veterinarian_contact_id) REFERENCES directory_contacts(id) ON DELETE SET NULL,
FOREIGN KEY(crematorium_contact_id) REFERENCES directory_contacts(id) ON DELETE SET NULL,
FOREIGN KEY(created_by) REFERENCES users(id) ON DELETE SET NULL,
FOREIGN KEY(updated_by) REFERENCES users(id) ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS idx_animal_deaths_date ON animal_deaths(deceased_date);
CREATE INDEX IF NOT EXISTS idx_animal_deaths_cause ON animal_deaths(cause_code, occurred_in_care);

View file

@ -0,0 +1,21 @@
CREATE TABLE IF NOT EXISTS app_settings (
setting_key TEXT PRIMARY KEY,
setting_value TEXT NOT NULL,
updated_by INTEGER,
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY(updated_by) REFERENCES users(id) ON DELETE SET NULL
);
INSERT OR IGNORE INTO app_settings(setting_key,setting_value) VALUES
('association_name','Association'),
('association_address',''),
('association_postal_code',''),
('association_city',''),
('association_phone',''),
('association_email',''),
('association_siret',''),
('association_rna',''),
('adoption_fee_sterilized','220'),
('adoption_fee_unsterilized','200'),
('quarantine_days','15'),
('backup_retention','10');

View file

@ -0,0 +1,5 @@
INSERT OR IGNORE INTO app_settings(setting_key,setting_value) VALUES
('project_description','Né de lhistoire de Twix, conçu pour protéger les suivants.'),
('development_year','2026'),
('developer_name',''),
('app_version','0.1');

View file

@ -0,0 +1,2 @@
DELETE FROM app_settings
WHERE setting_key IN ('project_description','development_year','developer_name','app_version');

View file

@ -0,0 +1,9 @@
UPDATE animal_movements
SET created_at=(SELECT d.deceased_date||' 12:00:00' FROM animal_deaths d WHERE d.animal_id=animal_movements.animal_id),
place=(SELECT CASE d.place_type WHEN 'refuge' THEN 'Au refuge' WHEN 'fa' THEN 'En famille d''accueil' WHEN 'veterinaire' THEN 'Chez le vétérinaire' WHEN 'exterieur' THEN 'À l''extérieur' WHEN 'inconnu' THEN 'Lieu inconnu' ELSE 'Autre lieu' END FROM animal_deaths d WHERE d.animal_id=animal_movements.animal_id),
contact_name=(SELECT v.name FROM animal_deaths d LEFT JOIN directory_contacts v ON v.id=d.veterinarian_contact_id WHERE d.animal_id=animal_movements.animal_id),
note=(SELECT 'Cause : '||CASE d.cause_code WHEN 'disease' THEN 'Maladie' WHEN 'accident' THEN 'Accident' WHEN 'old_age' THEN 'Vieillesse' WHEN 'congenital' THEN 'Malformation ou affection congénitale' WHEN 'surgery_complication' THEN 'Complication médicale ou chirurgicale' WHEN 'poisoning' THEN 'Empoisonnement' WHEN 'unknown' THEN 'Cause inconnue' ELSE 'Autre cause' END||CASE WHEN d.euthanized=1 THEN char(10)||'Euthanasie : oui' ELSE '' END||CASE WHEN c.name IS NOT NULL THEN char(10)||'Crématorium : '||c.name ELSE '' END FROM animal_deaths d LEFT JOIN directory_contacts c ON c.id=d.crematorium_contact_id WHERE d.animal_id=animal_movements.animal_id)
WHERE id IN (
SELECT MAX(m.id) FROM animal_movements m JOIN animal_deaths d ON d.animal_id=m.animal_id
WHERE m.kind='exit' AND lower(m.lieu) IN ('décès','deces','décès de l''animal','deces de l''animal') GROUP BY m.animal_id
);

View file

@ -0,0 +1,3 @@
UPDATE animal_movements
SET lieu='Décès de l''animal'
WHERE kind='exit' AND lower(lieu) IN ('décès','deces');

55
migrations/041_grants.sql Normal file
View file

@ -0,0 +1,55 @@
CREATE TABLE grant_years (
year INTEGER PRIMARY KEY CHECK(year BETWEEN 2000 AND 2200),
members_count INTEGER NOT NULL DEFAULT 0,
volunteers_count INTEGER NOT NULL DEFAULT 0,
employees_count INTEGER NOT NULL DEFAULT 0,
employees_fte REAL NOT NULL DEFAULT 0,
last_ag_date TEXT,
annual_report TEXT,
completed_actions TEXT,
planned_actions TEXT,
beneficiaries TEXT,
officers_notes TEXT,
in_kind_support TEXT,
treasury_amount REAL NOT NULL DEFAULT 0,
actual_expenses REAL NOT NULL DEFAULT 0,
actual_income REAL NOT NULL DEFAULT 0,
forecast_expenses REAL NOT NULL DEFAULT 0,
forecast_income REAL NOT NULL DEFAULT 0,
voluntary_contributions REAL NOT NULL DEFAULT 0,
updated_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE grant_applications (
id INTEGER PRIMARY KEY AUTOINCREMENT,
year INTEGER NOT NULL REFERENCES grant_years(year) ON DELETE CASCADE,
organization_name TEXT NOT NULL,
deadline TEXT,
requested_amount REAL NOT NULL DEFAULT 0,
previous_amount REAL NOT NULL DEFAULT 0,
awarded_amount REAL,
status TEXT NOT NULL DEFAULT 'draft' CHECK(status IN ('draft','ready','submitted','awarded','refused','withdrawn')),
notes TEXT,
created_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
updated_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_grant_applications_year ON grant_applications(year, deadline);
CREATE TABLE grant_documents (
id INTEGER PRIMARY KEY AUTOINCREMENT,
year INTEGER REFERENCES grant_years(year) ON DELETE SET NULL,
category TEXT NOT NULL,
title TEXT NOT NULL,
stored_name TEXT NOT NULL UNIQUE,
original_name TEXT NOT NULL,
mime_type TEXT NOT NULL,
size_bytes INTEGER NOT NULL,
valid_until TEXT,
notes TEXT,
uploaded_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_grant_documents_year ON grant_documents(year, category);

View file

@ -0,0 +1,15 @@
CREATE TABLE role_permissions (
role TEXT NOT NULL CHECK(role IN ('admin','responsable','benevole','lecture')),
module TEXT NOT NULL CHECK(module IN ('dashboard','animals','medical','care','directory','statistics','administrative','grants')),
can_view INTEGER NOT NULL DEFAULT 0 CHECK(can_view IN (0,1)),
can_edit INTEGER NOT NULL DEFAULT 0 CHECK(can_edit IN (0,1)),
updated_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
PRIMARY KEY(role,module)
);
INSERT INTO role_permissions(role,module,can_view,can_edit) VALUES
('admin','dashboard',1,1),('admin','animals',1,1),('admin','medical',1,1),('admin','care',1,1),('admin','directory',1,1),('admin','statistics',1,1),('admin','administrative',1,1),('admin','grants',1,1),
('responsable','dashboard',1,1),('responsable','animals',1,1),('responsable','medical',1,1),('responsable','care',1,1),('responsable','directory',1,1),('responsable','statistics',1,0),('responsable','administrative',1,1),('responsable','grants',1,1),
('benevole','dashboard',1,0),('benevole','animals',1,0),('benevole','medical',1,0),('benevole','care',1,1),('benevole','directory',1,0),('benevole','statistics',1,0),('benevole','administrative',0,0),('benevole','grants',0,0),
('lecture','dashboard',1,0),('lecture','animals',1,0),('lecture','medical',1,0),('lecture','care',0,0),('lecture','directory',0,0),('lecture','statistics',1,0),('lecture','administrative',0,0),('lecture','grants',0,0);

View file

@ -0,0 +1,2 @@
ALTER TABLE grant_documents ADD COLUMN application_id INTEGER REFERENCES grant_applications(id) ON DELETE SET NULL;
CREATE INDEX idx_grant_documents_application ON grant_documents(application_id, category);

View file

@ -0,0 +1,32 @@
CREATE TABLE animal_location_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
animal_id INTEGER NOT NULL,
from_status TEXT,
from_refuge_room TEXT,
from_care_box_key TEXT,
from_address TEXT,
to_status TEXT,
to_refuge_room TEXT,
to_care_box_key TEXT,
to_address TEXT,
reason TEXT,
source TEXT NOT NULL DEFAULT 'animal_form',
moved_at TEXT NOT NULL DEFAULT (datetime('now')),
user_id INTEGER,
FOREIGN KEY(animal_id) REFERENCES animals(id) ON DELETE CASCADE,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE SET NULL
);
CREATE INDEX idx_animal_location_history_animal_date
ON animal_location_history(animal_id, moved_at DESC, id DESC);
-- Les animaux déjà présents commencent avec un état de référence. Il ne s'agit
-- pas d'un déplacement inventé, mais du point de départ de leur future chronologie.
INSERT INTO animal_location_history (
animal_id, to_status, to_refuge_room, to_care_box_key, to_address,
reason, source, moved_at
)
SELECT id, status, refuge_room, care_box_key, current_address,
'État initial lors de lactivation de lhistorique', 'migration',
datetime('now')
FROM animals;

View file

@ -0,0 +1,8 @@
ALTER TABLE animals ADD COLUMN compatibility_dogs TEXT NOT NULL DEFAULT 'unknown'
CHECK (compatibility_dogs IN ('unknown','yes','no'));
ALTER TABLE animals ADD COLUMN compatibility_cats TEXT NOT NULL DEFAULT 'unknown'
CHECK (compatibility_cats IN ('unknown','yes','no'));
ALTER TABLE animals ADD COLUMN compatibility_children TEXT NOT NULL DEFAULT 'unknown'
CHECK (compatibility_children IN ('unknown','yes','no'));
ALTER TABLE animals ADD COLUMN house_trained TEXT NOT NULL DEFAULT 'unknown'
CHECK (house_trained IN ('unknown','yes','no'));

View file

@ -0,0 +1,16 @@
ALTER TABLE animals ADD COLUMN intake_type TEXT NOT NULL DEFAULT 'unknown'
CHECK (intake_type IN ('unknown','found','owner_surrender','transfer','born_in_care','return','seizure','other'));
ALTER TABLE animals ADD COLUMN intake_reason TEXT NOT NULL DEFAULT 'unknown'
CHECK (intake_reason IN ('unknown','stray_found','unwanted_litter','owner_health','owner_death','allergy','housing','financial','behavior','abandonment','danger','transfer','birth','other'));
ALTER TABLE animals ADD COLUMN depositor_contact_id INTEGER REFERENCES directory_contacts(id) ON DELETE SET NULL;
ALTER TABLE animals ADD COLUMN intake_circumstances TEXT;
CREATE INDEX idx_animals_intake_type ON animals(intake_type);
CREATE INDEX idx_animals_intake_reason ON animals(intake_reason);
CREATE INDEX idx_animals_depositor ON animals(depositor_contact_id);
INSERT INTO animal_movements(animal_id,kind,place,lieu,note,created_at)
SELECT a.id,'entry',COALESCE(NULLIF(trim(a.rescue_location_name),''),NULLIF(trim(a.rescue_address),'')),
'Entrée historique','Informations dentrée à compléter dans la fiche animale',
COALESCE(NULLIF(a.intake_date,'')||' 12:00:00',a.created_at,datetime('now'))
FROM animals a
WHERE NOT EXISTS(SELECT 1 FROM animal_movements m WHERE m.animal_id=a.id AND m.kind='entry');

View file

@ -0,0 +1,6 @@
INSERT INTO animal_movements(animal_id,kind,place,lieu,note,created_at)
SELECT a.id,'entry',COALESCE(NULLIF(trim(a.rescue_location_name),''),NULLIF(trim(a.rescue_address),'')),
'Entrée historique','Informations dentrée à compléter dans la fiche animale',
COALESCE(NULLIF(a.intake_date,'')||' 12:00:00',a.created_at,datetime('now'))
FROM animals a
WHERE NOT EXISTS(SELECT 1 FROM animal_movements m WHERE m.animal_id=a.id AND m.kind='entry');

View file

@ -0,0 +1,6 @@
ALTER TABLE vaccinations ADD COLUMN manufacturer TEXT;
ALTER TABLE vaccinations ADD COLUMN batch_expires_on TEXT;
ALTER TABLE vaccinations ADD COLUMN administered_by_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL;
ALTER TABLE vaccinations ADD COLUMN administered_by_name TEXT;
CREATE INDEX idx_vaccinations_batch_expiry ON vaccinations(batch_expires_on);
CREATE INDEX idx_vaccinations_administrator ON vaccinations(administered_by_user_id);

View file

@ -0,0 +1,21 @@
CREATE TABLE animal_placements (
id INTEGER PRIMARY KEY AUTOINCREMENT,
animal_id INTEGER NOT NULL,
event_type TEXT NOT NULL CHECK(event_type IN ('reservation','foster','adoption','return','cancellation')),
event_date TEXT NOT NULL,
contact_id INTEGER,
reason TEXT,
notes TEXT,
adoption_id INTEGER,
created_by INTEGER,
created_at TEXT NOT NULL DEFAULT(datetime('now')),
FOREIGN KEY(animal_id) REFERENCES animals(id) ON DELETE CASCADE,
FOREIGN KEY(contact_id) REFERENCES directory_contacts(id) ON DELETE SET NULL,
FOREIGN KEY(adoption_id) REFERENCES adoptions(id) ON DELETE SET NULL,
FOREIGN KEY(created_by) REFERENCES users(id) ON DELETE SET NULL
);
CREATE INDEX idx_animal_placements_animal_date ON animal_placements(animal_id,event_date DESC,id DESC);
CREATE UNIQUE INDEX idx_animal_placements_adoption ON animal_placements(adoption_id) WHERE adoption_id IS NOT NULL;
INSERT OR IGNORE INTO animal_placements(animal_id,event_type,event_date,contact_id,reason,notes,adoption_id,created_at)
SELECT animal_id,'adoption',adoption_date,adopter_contact_id,'Adoption définitive',notes,id,COALESCE(created_at,datetime('now')) FROM adoptions;

View file

@ -0,0 +1,7 @@
ALTER TABLE animals ADD COLUMN sterilization_status TEXT NOT NULL DEFAULT 'unknown'
CHECK(sterilization_status IN ('unknown','no','yes'));
ALTER TABLE animals ADD COLUMN sterilization_date TEXT;
ALTER TABLE animals ADD COLUMN sterilization_vet_contact_id INTEGER REFERENCES directory_contacts(id) ON DELETE SET NULL;
ALTER TABLE animals ADD COLUMN sterilization_clinic_contact_id INTEGER REFERENCES directory_contacts(id) ON DELETE SET NULL;
UPDATE animals SET sterilization_status=CASE WHEN sterilized=1 THEN 'yes' ELSE 'unknown' END;
CREATE INDEX idx_animals_sterilization_status ON animals(sterilization_status);

View file

@ -0,0 +1,14 @@
ALTER TABLE animals ADD COLUMN identification_type TEXT NOT NULL DEFAULT 'unknown'
CHECK(identification_type IN ('unknown','microchip','tattoo'));
ALTER TABLE animals ADD COLUMN identification_date TEXT;
ALTER TABLE animals ADD COLUMN identification_registration_status TEXT NOT NULL DEFAULT 'unknown'
CHECK(identification_registration_status IN ('unknown','pending','registered'));
ALTER TABLE animals ADD COLUMN adoption_availability TEXT NOT NULL DEFAULT 'unknown'
CHECK(adoption_availability IN ('unknown','not_available','available'));
ALTER TABLE animals ADD COLUMN adoption_available_from TEXT;
ALTER TABLE animals ADD COLUMN adoption_unavailability_reason TEXT;
UPDATE animals SET identification_type='microchip'
WHERE COALESCE(NULLIF(TRIM(chip_id),''),NULLIF(TRIM(chip),'')) IS NOT NULL;
CREATE INDEX idx_animals_identification_type ON animals(identification_type);
CREATE INDEX idx_animals_identification_registration ON animals(identification_registration_status);
CREATE INDEX idx_animals_adoption_availability ON animals(adoption_availability);

View file

@ -0,0 +1,21 @@
CREATE TABLE asm3_import_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source_name TEXT NOT NULL,
source_sha256 TEXT NOT NULL,
status TEXT NOT NULL CHECK(status IN ('completed','failed')),
summary_json TEXT NOT NULL,
backup_name TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE asm3_import_links (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source_sha256 TEXT NOT NULL,
entity_type TEXT NOT NULL,
source_id INTEGER NOT NULL,
target_id INTEGER NOT NULL,
import_run_id INTEGER NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE(source_sha256,entity_type,source_id),
FOREIGN KEY(import_run_id) REFERENCES asm3_import_runs(id) ON DELETE CASCADE
);
CREATE INDEX idx_asm3_import_links_target ON asm3_import_links(entity_type,target_id);

View file

@ -0,0 +1,13 @@
CREATE TABLE asm3_import_jobs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
original_name TEXT NOT NULL,
stored_name TEXT NOT NULL UNIQUE,
source_sha256 TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'analyzed' CHECK(status IN ('analyzed','completed','failed')),
analysis_json TEXT NOT NULL,
result_json TEXT,
created_by INTEGER,
created_at TEXT NOT NULL DEFAULT(datetime('now')),
completed_at TEXT,
FOREIGN KEY(created_by) REFERENCES users(id) ON DELETE SET NULL
);

View file

@ -0,0 +1,37 @@
CREATE TABLE IF NOT EXISTS ref_species (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT NOT NULL UNIQUE COLLATE NOCASE,
name TEXT NOT NULL,
category TEXT NOT NULL DEFAULT 'autre',
icon TEXT NOT NULL DEFAULT '🐾',
color TEXT NOT NULL DEFAULT '#6c757d',
active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0,1)),
sort_order INTEGER NOT NULL DEFAULT 100,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_ref_species_active_order
ON ref_species(active, sort_order, name);
INSERT OR IGNORE INTO ref_species(code,name,category,icon,color,sort_order) VALUES
('chat','Chat','animaux_compagnie','🐱','#6f8fb3',10),
('chien','Chien','animaux_compagnie','🐶','#b7791f',20),
('lapin','Lapin','nac','🐰','#8b6f47',30),
('furet','Furet','nac','🦦','#7c6651',40),
('cobaye','Cochon d''Inde','nac','🐹','#a06c46',50),
('oiseau','Oiseau','nac','🐦','#3b82a0',60),
('reptile','Reptile','nac','🦎','#56875a',70),
('animal-ferme','Animal de ferme','animal_ferme','🐐','#9a6b3f',80),
('faune-sauvage','Faune sauvage','faune_sauvage','🦅','#496b45',90),
('autre','Autre espèce','autre','🐾','#6c757d',100);
INSERT OR IGNORE INTO ref_species(code,name,category,icon,color,sort_order)
SELECT lower(trim(species)),
CASE WHEN trim(species)='' THEN 'Espèce inconnue' ELSE upper(substr(trim(species),1,1))||substr(trim(species),2) END,
'autre','🐾','#6c757d',95
FROM animals
WHERE trim(COALESCE(species,''))<>'';
UPDATE animals SET species='chat' WHERE lower(trim(species))='cat';
UPDATE animals SET species='chien' WHERE lower(trim(species))='dog';

View file

@ -0,0 +1,8 @@
ALTER TABLE ref_species ADD COLUMN is_system INTEGER NOT NULL DEFAULT 0 CHECK(is_system IN (0,1));
UPDATE ref_species SET is_system=1
WHERE code IN ('chat','chien','lapin','furet','cobaye','oiseau','reptile','animal-ferme','faune-sauvage','autre');
DELETE FROM ref_species
WHERE code IN ('cat','dog')
AND NOT EXISTS (SELECT 1 FROM animals a WHERE lower(trim(a.species))=ref_species.code);

View file

@ -0,0 +1,60 @@
ALTER TABLE animal_deaths RENAME TO animal_deaths_before_recovery;
CREATE TABLE animal_deaths (
animal_id INTEGER PRIMARY KEY,
deceased_date TEXT,
date_precision TEXT NOT NULL DEFAULT 'exact' CHECK(date_precision IN ('exact','approximate','unknown')),
death_age_value INTEGER,
death_age_unit TEXT CHECK(death_age_unit IN ('day','week','month','year')),
cause_code TEXT NOT NULL,
cause_details TEXT,
occurred_in_care INTEGER NOT NULL DEFAULT 1 CHECK(occurred_in_care IN (0,1)),
place_type TEXT NOT NULL DEFAULT 'refuge' CHECK(place_type IN ('refuge','fa','veterinaire','exterieur','inconnu','autre')),
place_details TEXT,
euthanized INTEGER NOT NULL DEFAULT 0 CHECK(euthanized IN (0,1)),
veterinarian_contact_id INTEGER,
crematorium_contact_id INTEGER,
body_disposition TEXT NOT NULL DEFAULT 'pending' CHECK(body_disposition IN ('pending','collective_cremation','individual_cremation','returned_family','burial','other')),
cremation_date TEXT,
memorial_medal_color TEXT CHECK(memorial_medal_color IN ('blue','pink')),
memorial_medal_count INTEGER NOT NULL DEFAULT 1 CHECK(memorial_medal_count >= 1),
recovered_source TEXT,
notes TEXT,
created_by INTEGER,
updated_by INTEGER,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY(animal_id) REFERENCES animals(id) ON DELETE CASCADE,
FOREIGN KEY(veterinarian_contact_id) REFERENCES directory_contacts(id) ON DELETE SET NULL,
FOREIGN KEY(crematorium_contact_id) REFERENCES directory_contacts(id) ON DELETE SET NULL,
FOREIGN KEY(created_by) REFERENCES users(id) ON DELETE SET NULL,
FOREIGN KEY(updated_by) REFERENCES users(id) ON DELETE SET NULL
);
INSERT INTO animal_deaths(animal_id,deceased_date,date_precision,cause_code,cause_details,occurred_in_care,place_type,place_details,euthanized,veterinarian_contact_id,crematorium_contact_id,body_disposition,cremation_date,notes,created_by,updated_by,created_at,updated_at)
SELECT animal_id,deceased_date,'exact',cause_code,cause_details,occurred_in_care,place_type,place_details,euthanized,veterinarian_contact_id,crematorium_contact_id,body_disposition,cremation_date,notes,created_by,updated_by,created_at,updated_at
FROM animal_deaths_before_recovery;
DROP TABLE animal_deaths_before_recovery;
CREATE INDEX idx_animal_deaths_date ON animal_deaths(deceased_date);
CREATE INDEX idx_animal_deaths_cause ON animal_deaths(cause_code, occurred_in_care);
CREATE TABLE recovery_import_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source TEXT NOT NULL,
backup_name TEXT,
summary_json TEXT,
created_by INTEGER,
created_at TEXT NOT NULL DEFAULT(datetime('now')),
FOREIGN KEY(created_by) REFERENCES users(id) ON DELETE SET NULL
);
CREATE TABLE recovery_import_links (
source_key TEXT PRIMARY KEY,
animal_id INTEGER NOT NULL,
run_id INTEGER NOT NULL,
action TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT(datetime('now')),
FOREIGN KEY(animal_id) REFERENCES animals(id) ON DELETE CASCADE,
FOREIGN KEY(run_id) REFERENCES recovery_import_runs(id) ON DELETE CASCADE
);

View file

@ -0,0 +1 @@
ALTER TABLE users ADD COLUMN animal_list_view TEXT NOT NULL DEFAULT 'table' CHECK(animal_list_view IN ('table','mosaic'));

View file

@ -0,0 +1,12 @@
-- Parcours des chats libres suivis par l'association.
ALTER TABLE animals ADD COLUMN free_cat_site_name TEXT;
ALTER TABLE animals ADD COLUMN free_cat_city TEXT;
ALTER TABLE animals ADD COLUMN free_cat_address TEXT;
ALTER TABLE animals ADD COLUMN free_cat_caretaker_contact_id INTEGER REFERENCES directory_contacts(id) ON DELETE SET NULL;
ALTER TABLE animals ADD COLUMN free_cat_captured_at TEXT;
ALTER TABLE animals ADD COLUMN free_cat_released_at TEXT;
ALTER TABLE animals ADD COLUMN free_cat_tracking_status TEXT NOT NULL DEFAULT 'present'
CHECK(free_cat_tracking_status IN ('present','missing','moved','deceased'));
CREATE INDEX IF NOT EXISTS idx_animals_free_cat_site ON animals(free_cat_site_name, free_cat_city);
CREATE INDEX IF NOT EXISTS idx_animals_free_cat_caretaker ON animals(free_cat_caretaker_contact_id);

View file

@ -0,0 +1,5 @@
-- Précise le départ durable du propriétaire en établissement sans casser la
-- contrainte historique de la famille de motif owner_health.
ALTER TABLE animals ADD COLUMN intake_owner_care_home INTEGER NOT NULL DEFAULT 0
CHECK(intake_owner_care_home IN (0,1));
CREATE INDEX IF NOT EXISTS idx_animals_intake_owner_care_home ON animals(intake_owner_care_home);

View file

@ -0,0 +1,2 @@
ALTER TABLE animal_deaths ADD COLUMN death_year INTEGER;
CREATE INDEX idx_animal_deaths_year ON animal_deaths(death_year);

View file

@ -0,0 +1,39 @@
ALTER TABLE animal_deaths RENAME TO animal_deaths_before_year_precision;
CREATE TABLE animal_deaths (
animal_id INTEGER PRIMARY KEY,
deceased_date TEXT,
date_precision TEXT NOT NULL DEFAULT 'exact' CHECK(date_precision IN ('exact','approximate','year','unknown')),
death_age_value INTEGER,
death_age_unit TEXT CHECK(death_age_unit IN ('day','week','month','year')),
cause_code TEXT NOT NULL,
cause_details TEXT,
occurred_in_care INTEGER NOT NULL DEFAULT 1 CHECK(occurred_in_care IN (0,1)),
place_type TEXT NOT NULL DEFAULT 'refuge' CHECK(place_type IN ('refuge','fa','veterinaire','exterieur','inconnu','autre')),
place_details TEXT,
euthanized INTEGER NOT NULL DEFAULT 0 CHECK(euthanized IN (0,1)),
veterinarian_contact_id INTEGER,
crematorium_contact_id INTEGER,
body_disposition TEXT NOT NULL DEFAULT 'pending' CHECK(body_disposition IN ('pending','collective_cremation','individual_cremation','returned_family','burial','other')),
cremation_date TEXT,
memorial_medal_color TEXT CHECK(memorial_medal_color IN ('blue','pink')),
memorial_medal_count INTEGER NOT NULL DEFAULT 1 CHECK(memorial_medal_count >= 1),
recovered_source TEXT,
notes TEXT,
created_by INTEGER,
updated_by INTEGER,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
death_year INTEGER CHECK(death_year BETWEEN 1900 AND 2200),
FOREIGN KEY(animal_id) REFERENCES animals(id) ON DELETE CASCADE,
FOREIGN KEY(veterinarian_contact_id) REFERENCES directory_contacts(id) ON DELETE SET NULL,
FOREIGN KEY(crematorium_contact_id) REFERENCES directory_contacts(id) ON DELETE SET NULL,
FOREIGN KEY(created_by) REFERENCES users(id) ON DELETE SET NULL,
FOREIGN KEY(updated_by) REFERENCES users(id) ON DELETE SET NULL
);
INSERT INTO animal_deaths SELECT * FROM animal_deaths_before_year_precision;
DROP TABLE animal_deaths_before_year_precision;
CREATE INDEX idx_animal_deaths_date ON animal_deaths(deceased_date);
CREATE INDEX idx_animal_deaths_year ON animal_deaths(death_year);
CREATE INDEX idx_animal_deaths_cause ON animal_deaths(cause_code,occurred_in_care);

View file

@ -0,0 +1,20 @@
CREATE TABLE dewormings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
animal_id INTEGER NOT NULL,
medication_id INTEGER,
administered_on TEXT NOT NULL,
dose_text TEXT,
weight_kg REAL,
next_due_date TEXT,
notes TEXT,
source_litter_id INTEGER,
created_by INTEGER,
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now')),
FOREIGN KEY(animal_id) REFERENCES animals(id) ON DELETE CASCADE,
FOREIGN KEY(medication_id) REFERENCES ref_medications(id) ON DELETE SET NULL,
FOREIGN KEY(source_litter_id) REFERENCES litters(id) ON DELETE SET NULL,
FOREIGN KEY(created_by) REFERENCES users(id) ON DELETE SET NULL
);
CREATE INDEX idx_dewormings_animal_date ON dewormings(animal_id,administered_on DESC);
CREATE INDEX idx_dewormings_next_due ON dewormings(next_due_date);

View file

@ -0,0 +1,18 @@
CREATE TABLE ref_dewormers (id INTEGER PRIMARY KEY AUTOINCREMENT,name TEXT NOT NULL COLLATE NOCASE UNIQUE,active_ingredient TEXT,form TEXT,notes TEXT,active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0,1)),created_at TEXT NOT NULL DEFAULT(datetime('now')));
INSERT INTO ref_dewormers(name,active_ingredient,form,notes) VALUES
('NexGard Combo',NULL,'Pipette','Couvre principalement : puces, tiques, acariens, vers ronds, ténias et certains vers pulmonaires.'),
('Broadline',NULL,'Pipette','Couvre principalement : puces, tiques, vers ronds et ténias.'),
('Milbemax Chat / Chaton',NULL,'Comprimé','Couvre principalement : vers ronds et ténias.'),
('Milprazon Chat / Chaton',NULL,'Comprimé','Équivalent générique du Milbemax ; couvre principalement les vers ronds et ténias.'),
('Drontal Chat',NULL,'Comprimé','Couvre principalement : vers ronds et ténias.'),
('Profender',NULL,'Pipette','Couvre principalement les vers ronds et ténias, sans action contre les puces ou les tiques.'),
('Panacur','Fenbendazole','Pâte / comprimé','Couvre principalement les vers ronds ; utilisé dans certains protocoles contre Giardia.'),
('Stronghold Plus',NULL,'Pipette','Couvre principalement les puces, tiques, gale ou acariens et certains vers ronds, mais pas les ténias.'),
('Advocate',NULL,'Pipette','Couvre principalement les puces, acariens et certains vers ronds, mais pas les ténias.');
INSERT OR IGNORE INTO ref_dewormers(name,active_ingredient,form) SELECT DISTINCT m.name,m.molecule,m.form FROM dewormings d JOIN ref_medications m ON m.id=d.medication_id;
ALTER TABLE dewormings RENAME TO dewormings_with_medications;
CREATE TABLE dewormings (id INTEGER PRIMARY KEY AUTOINCREMENT,animal_id INTEGER NOT NULL,dewormer_id INTEGER,administered_on TEXT NOT NULL,dose_text TEXT,weight_kg REAL,next_due_date TEXT,notes TEXT,source_litter_id INTEGER,created_by INTEGER,created_at TEXT NOT NULL DEFAULT(datetime('now')),updated_at TEXT NOT NULL DEFAULT(datetime('now')),FOREIGN KEY(animal_id) REFERENCES animals(id) ON DELETE CASCADE,FOREIGN KEY(dewormer_id) REFERENCES ref_dewormers(id) ON DELETE SET NULL,FOREIGN KEY(source_litter_id) REFERENCES litters(id) ON DELETE SET NULL,FOREIGN KEY(created_by) REFERENCES users(id) ON DELETE SET NULL);
INSERT INTO dewormings(id,animal_id,dewormer_id,administered_on,dose_text,weight_kg,next_due_date,notes,source_litter_id,created_by,created_at,updated_at) SELECT d.id,d.animal_id,r.id,d.administered_on,d.dose_text,d.weight_kg,d.next_due_date,d.notes,d.source_litter_id,d.created_by,d.created_at,d.updated_at FROM dewormings_with_medications d LEFT JOIN ref_medications m ON m.id=d.medication_id LEFT JOIN ref_dewormers r ON r.name=m.name COLLATE NOCASE;
DROP TABLE dewormings_with_medications;
CREATE INDEX idx_dewormings_animal_date ON dewormings(animal_id,administered_on DESC);
CREATE INDEX idx_dewormings_next_due ON dewormings(next_due_date);

View file

@ -0,0 +1,10 @@
INSERT OR IGNORE INTO ref_dewormers(name,active_ingredient,form,notes) VALUES
('NexGard Combo',NULL,'Pipette','Couvre principalement : puces, tiques, acariens, vers ronds, ténias et certains vers pulmonaires.'),
('Broadline',NULL,'Pipette','Couvre principalement : puces, tiques, vers ronds et ténias.'),
('Milbemax Chat / Chaton',NULL,'Comprimé','Couvre principalement : vers ronds et ténias.'),
('Milprazon Chat / Chaton',NULL,'Comprimé','Équivalent générique du Milbemax ; couvre principalement les vers ronds et ténias.'),
('Drontal Chat',NULL,'Comprimé','Couvre principalement : vers ronds et ténias.'),
('Profender',NULL,'Pipette','Couvre principalement les vers ronds et ténias, sans action contre les puces ou les tiques.'),
('Panacur','Fenbendazole','Pâte / comprimé','Couvre principalement les vers ronds ; utilisé dans certains protocoles contre Giardia.'),
('Stronghold Plus',NULL,'Pipette','Couvre principalement les puces, tiques, gale ou acariens et certains vers ronds, mais pas les ténias.'),
('Advocate',NULL,'Pipette','Couvre principalement les puces, acariens et certains vers ronds, mais pas les ténias.');

View file

@ -0,0 +1,25 @@
CREATE TABLE clinic_discount_rules (
id INTEGER PRIMARY KEY AUTOINCREMENT, clinic_contact_id INTEGER NOT NULL,
discount_group TEXT NOT NULL, label TEXT NOT NULL, percent REAL NOT NULL DEFAULT 0 CHECK(percent BETWEEN 0 AND 100),
UNIQUE(clinic_contact_id,discount_group), FOREIGN KEY(clinic_contact_id) REFERENCES directory_contacts(id) ON DELETE CASCADE
);
CREATE TABLE clinic_tariffs (
id INTEGER PRIMARY KEY AUTOINCREMENT, clinic_contact_id INTEGER NOT NULL, label TEXT NOT NULL COLLATE NOCASE,
category TEXT NOT NULL DEFAULT 'other', discount_group TEXT NOT NULL DEFAULT 'act', unit_label TEXT NOT NULL DEFAULT 'acte',
medication_id INTEGER, vaccine_id INTEGER, dewormer_id INTEGER, amount_cents INTEGER NOT NULL CHECK(amount_cents>=0),
active INTEGER NOT NULL DEFAULT 1 CHECK(active IN(0,1)), notes TEXT, created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')),
FOREIGN KEY(clinic_contact_id) REFERENCES directory_contacts(id) ON DELETE CASCADE,
FOREIGN KEY(medication_id) REFERENCES ref_medications(id) ON DELETE SET NULL,
FOREIGN KEY(vaccine_id) REFERENCES ref_vaccines(id) ON DELETE SET NULL,
FOREIGN KEY(dewormer_id) REFERENCES ref_dewormers(id) ON DELETE SET NULL, UNIQUE(clinic_contact_id,label)
);
CREATE INDEX idx_clinic_tariffs_clinic ON clinic_tariffs(clinic_contact_id,active,label);
CREATE TABLE animal_expenses (
id INTEGER PRIMARY KEY AUTOINCREMENT, animal_id INTEGER NOT NULL, clinic_contact_id INTEGER, tariff_id INTEGER,
source_type TEXT, source_id INTEGER, label TEXT NOT NULL, occurred_on TEXT NOT NULL, quantity REAL NOT NULL DEFAULT 1 CHECK(quantity>0),
catalog_unit_cents INTEGER NOT NULL CHECK(catalog_unit_cents>=0), discount_percent REAL NOT NULL DEFAULT 0,
total_cents INTEGER NOT NULL CHECK(total_cents>=0), notes TEXT, created_by INTEGER, created_at TEXT NOT NULL DEFAULT(datetime('now')),
FOREIGN KEY(animal_id) REFERENCES animals(id) ON DELETE CASCADE, FOREIGN KEY(clinic_contact_id) REFERENCES directory_contacts(id) ON DELETE SET NULL,
FOREIGN KEY(tariff_id) REFERENCES clinic_tariffs(id) ON DELETE SET NULL, FOREIGN KEY(created_by) REFERENCES users(id) ON DELETE SET NULL
);
CREATE INDEX idx_animal_expenses_animal_date ON animal_expenses(animal_id,occurred_on);

View file

@ -0,0 +1,2 @@
UPDATE directory_contacts SET organization_id=NULL,updated_at=datetime('now')
WHERE kind='organization' AND organization_id IS NOT NULL;

View file

@ -0,0 +1,6 @@
UPDATE medical_notes SET clinic_contact_id=(SELECT organization_id FROM directory_contacts WHERE id=medical_notes.vet_contact_id)
WHERE vet_contact_id IS NOT NULL AND EXISTS(SELECT 1 FROM directory_contacts WHERE id=medical_notes.vet_contact_id AND organization_id IS NOT NULL);
UPDATE treatments SET clinic_contact_id=(SELECT organization_id FROM directory_contacts WHERE id=treatments.vet_contact_id)
WHERE vet_contact_id IS NOT NULL AND EXISTS(SELECT 1 FROM directory_contacts WHERE id=treatments.vet_contact_id AND organization_id IS NOT NULL);
UPDATE vaccinations SET clinic_contact_id=(SELECT organization_id FROM directory_contacts WHERE id=vaccinations.vet_contact_id)
WHERE vet_contact_id IS NOT NULL AND EXISTS(SELECT 1 FROM directory_contacts WHERE id=vaccinations.vet_contact_id AND organization_id IS NOT NULL);

View file

@ -0,0 +1,10 @@
CREATE TABLE directory_contact_roles_new (
contact_id INTEGER NOT NULL,
role TEXT NOT NULL CHECK(role IN ('fa','benevole','dirigeant','veterinaire','cabinet','crematorium','adoptant','deposant','fourriere')),
PRIMARY KEY(contact_id,role),
FOREIGN KEY(contact_id) REFERENCES directory_contacts(id) ON DELETE CASCADE
);
INSERT INTO directory_contact_roles_new(contact_id,role) SELECT contact_id,role FROM directory_contact_roles;
DROP TABLE directory_contact_roles;
ALTER TABLE directory_contact_roles_new RENAME TO directory_contact_roles;
CREATE INDEX idx_directory_role ON directory_contact_roles(role,contact_id);

View file

@ -0,0 +1 @@
INSERT OR IGNORE INTO app_settings(setting_key,setting_value) VALUES('app_language','fr');

View file

@ -0,0 +1,54 @@
CREATE TABLE medical_prescriptions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
animal_id INTEGER NOT NULL REFERENCES animals(id) ON DELETE CASCADE,
prescribed_on TEXT NOT NULL,
veterinarian_contact_id INTEGER REFERENCES directory_contacts(id) ON DELETE SET NULL,
clinic_contact_id INTEGER REFERENCES directory_contacts(id) ON DELETE SET NULL,
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','finished','replaced')),
notes TEXT,
original_name TEXT NOT NULL,
stored_name TEXT NOT NULL,
mime_type TEXT NOT NULL,
size_bytes INTEGER NOT NULL DEFAULT 0,
created_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
created_at TEXT NOT NULL DEFAULT(datetime('now'))
);
CREATE INDEX idx_prescriptions_animal_date ON medical_prescriptions(animal_id,prescribed_on DESC,id DESC);
CREATE TABLE prescription_treatments (
prescription_id INTEGER NOT NULL REFERENCES medical_prescriptions(id) ON DELETE CASCADE,
treatment_id INTEGER NOT NULL REFERENCES treatments(id) ON DELETE CASCADE,
PRIMARY KEY(prescription_id,treatment_id)
);
CREATE TABLE lab_reports (
id INTEGER PRIMARY KEY AUTOINCREMENT,
animal_id INTEGER NOT NULL REFERENCES animals(id) ON DELETE CASCADE,
sampled_on TEXT NOT NULL,
report_type TEXT,
laboratory_name TEXT,
veterinarian_contact_id INTEGER REFERENCES directory_contacts(id) ON DELETE SET NULL,
clinic_contact_id INTEGER REFERENCES directory_contacts(id) ON DELETE SET NULL,
notes TEXT,
original_name TEXT,
stored_name TEXT,
mime_type TEXT,
size_bytes INTEGER NOT NULL DEFAULT 0,
created_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
created_at TEXT NOT NULL DEFAULT(datetime('now'))
);
CREATE INDEX idx_lab_reports_animal_date ON lab_reports(animal_id,sampled_on DESC,id DESC);
CREATE TABLE lab_results (
id INTEGER PRIMARY KEY AUTOINCREMENT,
report_id INTEGER NOT NULL REFERENCES lab_reports(id) ON DELETE CASCADE,
parameter_name TEXT NOT NULL,
value REAL NOT NULL,
unit TEXT,
reference_min REAL,
reference_max REAL,
notes TEXT,
position INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX idx_lab_results_report ON lab_results(report_id,position,id);
CREATE INDEX idx_lab_results_parameter ON lab_results(parameter_name COLLATE NOCASE);

View file

@ -0,0 +1,22 @@
CREATE TABLE lab_reference_ranges (
id INTEGER PRIMARY KEY AUTOINCREMENT,
analyzer_code TEXT NOT NULL,
analyzer_name TEXT NOT NULL,
species_code TEXT NOT NULL,
parameter_code TEXT NOT NULL,
parameter_name TEXT NOT NULL,
unit TEXT NOT NULL,
reference_min REAL,
reference_max REAL,
source_note TEXT,
active INTEGER NOT NULL DEFAULT 1,
sort_order INTEGER NOT NULL DEFAULT 100,
updated_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
updated_at TEXT NOT NULL DEFAULT(datetime('now')),
UNIQUE(analyzer_code,species_code,parameter_code)
);
CREATE INDEX idx_lab_reference_lookup ON lab_reference_ranges(analyzer_code,species_code,active,sort_order);
INSERT INTO lab_reference_ranges(analyzer_code,analyzer_name,species_code,parameter_code,parameter_name,unit,reference_min,reference_max,source_note,sort_order) VALUES
('vetscan-hm5','Zoetis VETSCAN HM5','chat','GB','Leucocytes','10⁹/L',3.50,20.70,'Fiche Zoetis actuelle',10),('vetscan-hm5','Zoetis VETSCAN HM5','chat','LYM','Lymphocytes','10⁹/L',0.83,9.10,'Fiche Zoetis actuelle',20),('vetscan-hm5','Zoetis VETSCAN HM5','chat','MON','Monocytes','10⁹/L',0.09,1.21,'Fiche Zoetis actuelle',30),('vetscan-hm5','Zoetis VETSCAN HM5','chat','NEU','Neutrophiles','10⁹/L',1.63,13.37,'Fiche Zoetis actuelle',40),('vetscan-hm5','Zoetis VETSCAN HM5','chat','EOS','Éosinophiles','10⁹/L',0.02,0.49,'Fiche Zoetis actuelle',50),('vetscan-hm5','Zoetis VETSCAN HM5','chat','BAS','Basophiles','10⁹/L',NULL,NULL,'Aucun intervalle constructeur publié',60),('vetscan-hm5','Zoetis VETSCAN HM5','chat','GR','Érythrocytes','10¹²/L',7.70,12.80,'Fiche Zoetis actuelle',70),('vetscan-hm5','Zoetis VETSCAN HM5','chat','HGB','Hémoglobine','g/dL',10,17,'Fiche Zoetis actuelle',80),('vetscan-hm5','Zoetis VETSCAN HM5','chat','HCT','Hématocrite','%',33.7,55.4,'Fiche Zoetis actuelle',90),('vetscan-hm5','Zoetis VETSCAN HM5','chat','VCM','Volume corpusculaire moyen','fL',35,52,'Fiche Zoetis actuelle',100),('vetscan-hm5','Zoetis VETSCAN HM5','chat','HCM','Hémoglobine corpusculaire moyenne','pg',10,16.9,'Fiche Zoetis actuelle',110),('vetscan-hm5','Zoetis VETSCAN HM5','chat','CCMH','Concentration corpusculaire moyenne en Hb','g/dL',27,35,'Fiche Zoetis actuelle',120),('vetscan-hm5','Zoetis VETSCAN HM5','chat','IDRc','RDW-CV','%',18.3,24.1,'Fiche Zoetis actuelle',130),('vetscan-hm5','Zoetis VETSCAN HM5','chat','IDRs','RDW-SD','fL',NULL,NULL,'Aucun intervalle constructeur publié',140),('vetscan-hm5','Zoetis VETSCAN HM5','chat','PLT','Plaquettes','10⁹/L',125,618,'Fiche Zoetis actuelle',150),('vetscan-hm5','Zoetis VETSCAN HM5','chat','VPM','Volume plaquettaire moyen','fL',8.6,14.9,'Fiche Zoetis actuelle',160),('vetscan-hm5','Zoetis VETSCAN HM5','chat','PCT','Thrombocrite','%',NULL,NULL,'Calculé sans intervalle constructeur publié',170),('vetscan-hm5','Zoetis VETSCAN HM5','chat','IDPc','PDW-CV','%',NULL,NULL,'Calculé sans intervalle constructeur publié',180),('vetscan-hm5','Zoetis VETSCAN HM5','chat','IDPs','PDW-SD','fL',NULL,NULL,'Calculé sans intervalle constructeur publié',190),
('vetscan-hm5','Zoetis VETSCAN HM5','chien','GB','Leucocytes','10⁹/L',6,17,'Fiche Zoetis actuelle',10),('vetscan-hm5','Zoetis VETSCAN HM5','chien','LYM','Lymphocytes','10⁹/L',1,4.8,'Fiche Zoetis actuelle',20),('vetscan-hm5','Zoetis VETSCAN HM5','chien','MON','Monocytes','10⁹/L',0.2,1.5,'Fiche Zoetis actuelle',30),('vetscan-hm5','Zoetis VETSCAN HM5','chien','NEU','Neutrophiles','10⁹/L',3,12,'Fiche Zoetis actuelle',40),('vetscan-hm5','Zoetis VETSCAN HM5','chien','EOS','Éosinophiles','10⁹/L',0,0.8,'Fiche Zoetis actuelle',50),('vetscan-hm5','Zoetis VETSCAN HM5','chien','BAS','Basophiles','10⁹/L',NULL,NULL,'Aucun intervalle constructeur publié',60),('vetscan-hm5','Zoetis VETSCAN HM5','chien','GR','Érythrocytes','10¹²/L',5.5,8.5,'Fiche Zoetis actuelle',70),('vetscan-hm5','Zoetis VETSCAN HM5','chien','HGB','Hémoglobine','g/dL',12,18,'Fiche Zoetis actuelle',80),('vetscan-hm5','Zoetis VETSCAN HM5','chien','HCT','Hématocrite','%',37,55,'Fiche Zoetis actuelle',90),('vetscan-hm5','Zoetis VETSCAN HM5','chien','VCM','Volume corpusculaire moyen','fL',60,77,'Fiche Zoetis actuelle',100),('vetscan-hm5','Zoetis VETSCAN HM5','chien','HCM','Hémoglobine corpusculaire moyenne','pg',19.5,24.5,'Fiche Zoetis actuelle',110),('vetscan-hm5','Zoetis VETSCAN HM5','chien','CCMH','Concentration corpusculaire moyenne en Hb','g/dL',31,39,'Fiche Zoetis actuelle',120),('vetscan-hm5','Zoetis VETSCAN HM5','chien','IDRc','RDW-CV','%',14,20,'Fiche Zoetis actuelle',130),('vetscan-hm5','Zoetis VETSCAN HM5','chien','IDRs','RDW-SD','fL',NULL,NULL,'Aucun intervalle constructeur publié',140),('vetscan-hm5','Zoetis VETSCAN HM5','chien','PLT','Plaquettes','10⁹/L',165,500,'Fiche Zoetis actuelle',150),('vetscan-hm5','Zoetis VETSCAN HM5','chien','VPM','Volume plaquettaire moyen','fL',3.9,11.1,'Fiche Zoetis actuelle',160),('vetscan-hm5','Zoetis VETSCAN HM5','chien','PCT','Thrombocrite','%',NULL,NULL,'Calculé sans intervalle constructeur publié',170),('vetscan-hm5','Zoetis VETSCAN HM5','chien','IDPc','PDW-CV','%',NULL,NULL,'Calculé sans intervalle constructeur publié',180),('vetscan-hm5','Zoetis VETSCAN HM5','chien','IDPs','PDW-SD','fL',NULL,NULL,'Calculé sans intervalle constructeur publié',190);

View file

@ -0,0 +1,11 @@
ALTER TABLE animals ADD COLUMN website_published INTEGER NOT NULL DEFAULT 0 CHECK(website_published IN (0,1));
ALTER TABLE animals ADD COLUMN website_description TEXT;
CREATE INDEX idx_animals_website ON animals(website_published,adoption_availability,species);
INSERT OR IGNORE INTO app_settings(setting_key,setting_value) VALUES
('public_site_enabled','0'),('public_site_theme','warm'),
('public_site_home_title','Ils attendent une famille'),('public_site_home_text','Découvrez les animaux actuellement proposés à ladoption par notre association.'),
('public_site_about_enabled','1'),('public_site_about_title','Notre association'),('public_site_about_text','Présentez ici votre association, son histoire et ses missions.'),
('public_site_donation_title','Nous soutenir'),('public_site_donation_text','Chaque don contribue directement aux soins, à lalimentation et à la protection des animaux.'),('public_site_donation_url',''),
('public_site_contact_text','Une question sur une adoption ou sur lassociation ? Contactez-nous.'),
('public_site_facebook_url',''),('public_site_instagram_url','');

View file

@ -0,0 +1 @@
INSERT OR IGNORE INTO app_settings(setting_key,setting_value) VALUES('public_site_routing','integrated');

View file

@ -0,0 +1,3 @@
INSERT OR IGNORE INTO app_settings(setting_key,setting_value) VALUES
('association_latitude',''),
('association_longitude','');

View file

@ -0,0 +1,5 @@
INSERT OR IGNORE INTO app_settings(setting_key,setting_value) VALUES
('notification_email_enabled','0'),
('notification_email_frequency','weekly'),
('notification_email_recipient',''),
('notification_last_sent_at','');

View file

@ -0,0 +1,17 @@
ALTER TABLE directory_contacts ADD COLUMN anonymized_at TEXT;
ALTER TABLE directory_contacts ADD COLUMN anonymization_reason TEXT;
CREATE TABLE IF NOT EXISTS private_file_access_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
resource_type TEXT NOT NULL,
resource_id INTEGER,
animal_id INTEGER REFERENCES animals(id) ON DELETE SET NULL,
action TEXT NOT NULL DEFAULT 'view',
original_name TEXT,
ip_address TEXT,
user_agent TEXT,
accessed_at TEXT NOT NULL DEFAULT(datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_private_access_date ON private_file_access_log(accessed_at DESC);
CREATE INDEX IF NOT EXISTS idx_private_access_resource ON private_file_access_log(resource_type,resource_id);

View file

@ -0,0 +1,14 @@
CREATE TABLE IF NOT EXISTS trusted_devices (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
selector TEXT NOT NULL UNIQUE,
token_hash TEXT NOT NULL,
device_name TEXT NOT NULL,
user_agent TEXT,
ip_address TEXT,
created_at TEXT NOT NULL DEFAULT(datetime('now')),
last_used_at TEXT NOT NULL DEFAULT(datetime('now')),
expires_at TEXT NOT NULL,
revoked_at TEXT
);
CREATE INDEX IF NOT EXISTS idx_trusted_devices_user ON trusted_devices(user_id,revoked_at,expires_at);

View file

@ -0,0 +1,23 @@
UPDATE lab_reference_ranges SET analyzer_name = 'Bilan complet' WHERE analyzer_code = 'vetscan-hm5';
INSERT INTO lab_reference_ranges(analyzer_code,analyzer_name,species_code,parameter_code,parameter_name,unit,reference_min,reference_max,source_note,sort_order) VALUES
('renal','Bilan rénal','chat','CRE','Créatinine','mg/L',3,21,'Zoetis VETSCAN VS2 — conversion depuis mg/dL',10),
('renal','Bilan rénal','chat','UREE','Urée','g/L',0.214,0.642,'Zoetis VETSCAN VS2 — conversion du BUN',20),
('renal','Bilan rénal','chat','SDMA','SDMA','µg/dL',0,14,'IDEXX SDMA — chat adulte',30),
('renal','Bilan rénal','chat','PHOS','Phosphore','mg/L',34,85,'Zoetis VETSCAN VS2 — conversion depuis mg/dL',40),
('renal','Bilan rénal','chien','CRE','Créatinine','mg/L',3,14,'Zoetis VETSCAN VS2 — conversion depuis mg/dL',10),
('renal','Bilan rénal','chien','UREE','Urée','g/L',0.150,0.535,'Zoetis VETSCAN VS2 — conversion du BUN',20),
('renal','Bilan rénal','chien','SDMA','SDMA','µg/dL',0,14,'IDEXX SDMA — chien adulte',30),
('renal','Bilan rénal','chien','PHOS','Phosphore','mg/L',29,66,'Zoetis VETSCAN VS2 — conversion depuis mg/dL',40),
('hepatic','Bilan hépatique','chat','ALT','ALAT','U/L',20,100,'Zoetis VETSCAN VS2',10),
('hepatic','Bilan hépatique','chat','ALP','PAL','U/L',10,90,'Zoetis VETSCAN VS2',20),
('hepatic','Bilan hépatique','chat','TBIL','Bilirubine totale','mg/L',1,6,'Zoetis VETSCAN VS2 — conversion depuis mg/dL',30),
('hepatic','Bilan hépatique','chat','ALB','Albumine','g/L',22,44,'Zoetis VETSCAN VS2',40),
('hepatic','Bilan hépatique','chien','ALT','ALAT','U/L',10,118,'Zoetis VETSCAN VS2',10),
('hepatic','Bilan hépatique','chien','ALP','PAL','U/L',20,150,'Zoetis VETSCAN VS2',20),
('hepatic','Bilan hépatique','chien','TBIL','Bilirubine totale','mg/L',1,6,'Zoetis VETSCAN VS2 — conversion depuis mg/dL',30),
('hepatic','Bilan hépatique','chien','ALB','Albumine','g/L',25,44,'Zoetis VETSCAN VS2',40),
('thyroid','Bilan thyroïdien','chat','T4','T4 totale','µg/dL',1.5,4.8,'Zoetis VETSCAN VS2',10),
('thyroid','Bilan thyroïdien','chat','TSH','TSH','ng/mL',NULL,NULL,'Aucun intervalle VETSCAN VS2 publié',20),
('thyroid','Bilan thyroïdien','chien','T4','T4 totale','µg/dL',1.1,4.0,'Zoetis VETSCAN VS2',10),
('thyroid','Bilan thyroïdien','chien','TSH','TSH','ng/mL',NULL,NULL,'Aucun intervalle VETSCAN VS2 publié',20);

View file

@ -0,0 +1,45 @@
CREATE TABLE IF NOT EXISTS inventory_products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
category TEXT NOT NULL CHECK(category IN ('medication','dewormer','vaccine')),
reference_id INTEGER,
name TEXT NOT NULL,
unit TEXT NOT NULL DEFAULT 'unité',
minimum_quantity REAL NOT NULL DEFAULT 0 CHECK(minimum_quantity >= 0),
active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0,1)),
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now')),
UNIQUE(category,reference_id),
UNIQUE(category,name)
);
CREATE TABLE IF NOT EXISTS inventory_batches (
id INTEGER PRIMARY KEY AUTOINCREMENT,
product_id INTEGER NOT NULL REFERENCES inventory_products(id) ON DELETE CASCADE,
batch_number TEXT,
expires_on TEXT,
supplier_contact_id INTEGER REFERENCES directory_contacts(id) ON DELETE SET NULL,
unit_cost_cents INTEGER CHECK(unit_cost_cents IS NULL OR unit_cost_cents >= 0),
quantity REAL NOT NULL DEFAULT 0,
opened_on TEXT,
notes TEXT,
created_at TEXT NOT NULL DEFAULT(datetime('now'))
);
CREATE TABLE IF NOT EXISTS inventory_movements (
id INTEGER PRIMARY KEY AUTOINCREMENT,
product_id INTEGER NOT NULL REFERENCES inventory_products(id) ON DELETE CASCADE,
batch_id INTEGER REFERENCES inventory_batches(id) ON DELETE SET NULL,
movement_type TEXT NOT NULL CHECK(movement_type IN ('entry','administration','loss','correction')),
quantity_delta REAL NOT NULL CHECK(quantity_delta <> 0),
animal_id INTEGER REFERENCES animals(id) ON DELETE SET NULL,
occurred_at TEXT NOT NULL DEFAULT(datetime('now')),
reason TEXT,
created_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
created_at TEXT NOT NULL DEFAULT(datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_inventory_batches_product ON inventory_batches(product_id,expires_on);
CREATE INDEX IF NOT EXISTS idx_inventory_movements_product ON inventory_movements(product_id,occurred_at DESC);
INSERT OR IGNORE INTO app_settings(setting_key,setting_value) VALUES
('backup_schedule_enabled','0'),('backup_schedule_frequency','daily'),('backup_last_run_at',''),('backup_last_verified_at',''),
('privacy_retention_adopter_years','5'),('privacy_retention_foster_years','5'),('privacy_retention_volunteer_years','5'),('privacy_access_log_months','24');

41
migrations/080_agenda.sql Normal file
View file

@ -0,0 +1,41 @@
ALTER TABLE role_permissions RENAME TO role_permissions_legacy;
CREATE TABLE role_permissions (
role TEXT NOT NULL CHECK(role IN ('admin','responsable','benevole','lecture')),
module TEXT NOT NULL CHECK(module IN ('dashboard','animals','medical','care','directory','agenda','statistics','administrative','grants')),
can_view INTEGER NOT NULL DEFAULT 0 CHECK(can_view IN (0,1)),
can_edit INTEGER NOT NULL DEFAULT 0 CHECK(can_edit IN (0,1)),
updated_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
updated_at TEXT NOT NULL DEFAULT(datetime('now')),
PRIMARY KEY(role,module)
);
INSERT INTO role_permissions SELECT * FROM role_permissions_legacy;
DROP TABLE role_permissions_legacy;
INSERT INTO role_permissions(role,module,can_view,can_edit) VALUES
('admin','agenda',1,1),('responsable','agenda',1,1),('benevole','agenda',1,1),('lecture','agenda',1,0);
CREATE TABLE agenda_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
item_type TEXT NOT NULL CHECK(item_type IN ('appointment','veterinary','transport','pre_adoption','task','other')),
title TEXT NOT NULL,
description TEXT,
starts_at TEXT NOT NULL,
ends_at TEXT,
all_day INTEGER NOT NULL DEFAULT 0 CHECK(all_day IN (0,1)),
location TEXT,
status TEXT NOT NULL DEFAULT 'planned' CHECK(status IN ('planned','completed','cancelled')),
priority TEXT NOT NULL DEFAULT 'normal' CHECK(priority IN ('low','normal','high','urgent')),
assigned_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
animal_id INTEGER REFERENCES animals(id) ON DELETE SET NULL,
contact_id INTEGER REFERENCES directory_contacts(id) ON DELETE SET NULL,
reminder_minutes INTEGER CHECK(reminder_minutes IS NULL OR reminder_minutes >= 0),
repeat_rule TEXT NOT NULL DEFAULT 'none' CHECK(repeat_rule IN ('none','daily','weekly','monthly')),
repeat_until TEXT,
completed_at TEXT,
completed_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
created_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
);
CREATE INDEX idx_agenda_starts ON agenda_items(starts_at,status);
CREATE INDEX idx_agenda_assignee ON agenda_items(assigned_user_id,status,starts_at);
CREATE INDEX idx_agenda_animal ON agenda_items(animal_id,starts_at);

View file

@ -0,0 +1 @@
ALTER TABLE users ADD COLUMN agenda_view TEXT NOT NULL DEFAULT 'month' CHECK(agenda_view IN ('month','week'));

View file

@ -0,0 +1,3 @@
ALTER TABLE users ADD COLUMN agenda_feed_token_hash TEXT;
ALTER TABLE users ADD COLUMN agenda_feed_created_at TEXT;
CREATE UNIQUE INDEX idx_users_agenda_feed_token ON users(agenda_feed_token_hash) WHERE agenda_feed_token_hash IS NOT NULL;

View file

@ -0,0 +1,6 @@
CREATE TABLE agenda_volunteers (
agenda_item_id INTEGER NOT NULL REFERENCES agenda_items(id) ON DELETE CASCADE,
contact_id INTEGER NOT NULL REFERENCES directory_contacts(id) ON DELETE CASCADE,
PRIMARY KEY(agenda_item_id,contact_id)
);
CREATE INDEX idx_agenda_volunteers_contact ON agenda_volunteers(contact_id,agenda_item_id);

View file

@ -0,0 +1,45 @@
PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS shelter_rooms (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
room_type TEXT NOT NULL DEFAULT 'collective' CHECK(room_type IN ('collective','boxes')),
status_code TEXT NOT NULL DEFAULT 'refuge' CHECK(status_code IN ('refuge','soin','quarantaine')),
color TEXT NOT NULL DEFAULT '#6c757d',
bulk_validation INTEGER NOT NULL DEFAULT 0,
active INTEGER NOT NULL DEFAULT 1,
sort_order INTEGER NOT NULL DEFAULT 100,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS shelter_boxes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
room_id INTEGER NOT NULL,
code TEXT NOT NULL,
name TEXT NOT NULL,
active INTEGER NOT NULL DEFAULT 1,
sort_order INTEGER NOT NULL DEFAULT 100,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE(room_id, code),
FOREIGN KEY(room_id) REFERENCES shelter_rooms(id) ON DELETE RESTRICT
);
INSERT OR IGNORE INTO shelter_rooms(code,name,room_type,status_code,color,bulk_validation,active,sort_order) VALUES
('silver','Salle collective 1','collective','refuge','#adb5bd',1,1,10),
('twix','Salle collective 2','collective','refuge','#f0a35e',1,1,20),
('care','Infirmerie','boxes','soin','#e58a8a',0,1,30),
('quarantine','Salle de quarantaine','boxes','quarantaine','#d8bd52',0,1,40);
INSERT OR IGNORE INTO shelter_boxes(room_id,code,name,sort_order)
SELECT id,'top_a','Box supérieur gauche',10 FROM shelter_rooms WHERE code IN ('care','quarantine');
INSERT OR IGNORE INTO shelter_boxes(room_id,code,name,sort_order)
SELECT id,'top_b','Box supérieur central',20 FROM shelter_rooms WHERE code IN ('care','quarantine');
INSERT OR IGNORE INTO shelter_boxes(room_id,code,name,sort_order)
SELECT id,'top_c','Box supérieur droit',30 FROM shelter_rooms WHERE code IN ('care','quarantine');
INSERT OR IGNORE INTO shelter_boxes(room_id,code,name,sort_order)
SELECT id,'bottom_a','Box inférieur gauche',40 FROM shelter_rooms WHERE code IN ('care','quarantine');
INSERT OR IGNORE INTO shelter_boxes(room_id,code,name,sort_order)
SELECT id,'bottom_b','Box inférieur droit',50 FROM shelter_rooms WHERE code IN ('care','quarantine');

View file

@ -0,0 +1,8 @@
CREATE TABLE IF NOT EXISTS login_attempts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username_hash TEXT NOT NULL,
ip_address TEXT,
succeeded INTEGER NOT NULL DEFAULT 0,
attempted_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_login_attempts_time_ip ON login_attempts(attempted_at,ip_address);

View file

@ -0,0 +1,8 @@
ALTER TABLE animals ADD COLUMN rescue_location_confidence TEXT;
ALTER TABLE animals ADD COLUMN rescue_location_evidence TEXT;
UPDATE animals
SET rescue_location_confidence='documented',
rescue_location_evidence=COALESCE(NULLIF(TRIM(rescue_location_evidence),''),'Lieu saisi ou repris depuis une source historique explicite')
WHERE (TRIM(COALESCE(rescue_location_name,''))<>'' OR TRIM(COALESCE(rescue_address,''))<>'')
AND TRIM(COALESCE(rescue_location_confidence,''))='';

View file

@ -0,0 +1,52 @@
CREATE TABLE animal_surgeries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
animal_id INTEGER NOT NULL REFERENCES animals(id) ON DELETE CASCADE,
category TEXT NOT NULL,
procedure_name TEXT NOT NULL,
surgery_date TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'completed' CHECK(status IN ('planned','completed','cancelled')),
clinic_contact_id INTEGER REFERENCES directory_contacts(id) ON DELETE SET NULL,
veterinarian_contact_id INTEGER REFERENCES directory_contacts(id) ON DELETE SET NULL,
tariff_id INTEGER REFERENCES clinic_tariffs(id) ON DELETE SET NULL,
amount_cents INTEGER CHECK(amount_cents IS NULL OR amount_cents>=0),
anesthesia TEXT, notes TEXT, follow_up TEXT,
created_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now'))
);
CREATE INDEX idx_animal_surgeries_animal_date ON animal_surgeries(animal_id,surgery_date);
CREATE TABLE accounting_invoices (
id INTEGER PRIMARY KEY AUTOINCREMENT,
vendor_contact_id INTEGER REFERENCES directory_contacts(id) ON DELETE SET NULL,
reference TEXT NOT NULL COLLATE NOCASE,
invoice_date TEXT NOT NULL, due_date TEXT,
document_type TEXT NOT NULL DEFAULT 'invoice' CHECK(document_type IN ('invoice','credit','adjustment')),
status TEXT NOT NULL DEFAULT 'to_pay' CHECK(status IN ('to_pay','partial','paid','disputed','cancelled','credit')),
payment_method TEXT CHECK(payment_method IS NULL OR payment_method IN ('transfer','card','check','cash','direct_debit','other')),
paid_on TEXT,
total_ht_cents INTEGER,
total_ttc_cents INTEGER NOT NULL,
amount_due_cents INTEGER,
notes TEXT, source TEXT,
original_filename TEXT, stored_filename TEXT, mime_type TEXT, file_size INTEGER,
created_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')),
UNIQUE(vendor_contact_id,reference)
);
CREATE INDEX idx_accounting_invoice_date ON accounting_invoices(invoice_date,status);
CREATE INDEX idx_accounting_invoice_vendor ON accounting_invoices(vendor_contact_id,invoice_date);
ALTER TABLE role_permissions RENAME TO role_permissions_legacy;
CREATE TABLE role_permissions (
role TEXT NOT NULL CHECK(role IN ('admin','responsable','benevole','lecture')),
module TEXT NOT NULL CHECK(module IN ('dashboard','animals','medical','care','directory','agenda','statistics','administrative','accounting','grants')),
can_view INTEGER NOT NULL DEFAULT 0 CHECK(can_view IN (0,1)),
can_edit INTEGER NOT NULL DEFAULT 0 CHECK(can_edit IN (0,1)),
updated_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
updated_at TEXT NOT NULL DEFAULT(datetime('now')),
PRIMARY KEY(role,module)
);
INSERT INTO role_permissions SELECT * FROM role_permissions_legacy;
DROP TABLE role_permissions_legacy;
INSERT INTO role_permissions(role,module,can_view,can_edit) VALUES
('admin','accounting',1,1),('responsable','accounting',1,1),('benevole','accounting',0,0),('lecture','accounting',0,0);

View file

@ -0,0 +1,3 @@
ALTER TABLE accounting_invoices ADD COLUMN deleted_at TEXT;
ALTER TABLE accounting_invoices ADD COLUMN deleted_by INTEGER REFERENCES users(id) ON DELETE SET NULL;
CREATE INDEX idx_accounting_invoices_deleted ON accounting_invoices(deleted_at,invoice_date);

View file

@ -0,0 +1,12 @@
CREATE TABLE IF NOT EXISTS medical_photos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
animal_id INTEGER NOT NULL,
medical_note_id INTEGER NOT NULL,
filename TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY(animal_id) REFERENCES animals(id),
FOREIGN KEY(medical_note_id) REFERENCES medical_notes(id)
);
CREATE INDEX IF NOT EXISTS idx_medical_photos_animal ON medical_photos(animal_id);
CREATE INDEX IF NOT EXISTS idx_medical_photos_note ON medical_photos(medical_note_id);