diff --git a/docs/MCP_CLAUDE.md b/docs/MCP_CLAUDE.md
index 78b7ccc..651396c 100644
--- a/docs/MCP_CLAUDE.md
+++ b/docs/MCP_CLAUDE.md
@@ -9,7 +9,7 @@ messagerie, journal d'audit et suivi des mises à jour.
| Domaine | Capacités |
|---|---|
-| **Événements** | Lister, lire, créer, modifier, supprimer ; appliquer un modèle de check-list ; copier à une autre date. Deux textes distincts : `description` reste interne à l'équipe, `publicDescription` est affichée aux visiteurs |
+| **Événements** | Lister, lire, créer, modifier, supprimer ; appliquer un modèle de check-list ; copier à une autre date. Deux textes distincts : `description` reste interne à l'équipe, `publicDescription` est affichée aux visiteurs. `ticketingUrl` porte le lien de billetterie en ligne (HelloAsso…), présenté aux familles comme « je réserve ma place », distinct des créneaux de bénévoles |
| **Check-lists** | Lister les tâches de tous les événements, créer, modifier, supprimer, réorganiser ; gérer les modèles réutilisables, en créer un depuis un événement ou en remplacer un |
| **Bénévoles** | Créneaux (créer, modifier, supprimer), inscriptions (lister, inscrire manuellement, annuler) |
| **Pièces jointes** | Lister, rattacher et retirer les documents d'un événement |
diff --git a/drizzle/0017_event_ticketing_url.sql b/drizzle/0017_event_ticketing_url.sql
new file mode 100644
index 0000000..e73091e
--- /dev/null
+++ b/drizzle/0017_event_ticketing_url.sql
@@ -0,0 +1 @@
+ALTER TABLE "events" ADD COLUMN "ticketing_url" text;
\ No newline at end of file
diff --git a/drizzle/0018_whatsapp_group_link.sql b/drizzle/0018_whatsapp_group_link.sql
new file mode 100644
index 0000000..b44eb9f
--- /dev/null
+++ b/drizzle/0018_whatsapp_group_link.sql
@@ -0,0 +1 @@
+ALTER TABLE "association_settings" ADD COLUMN "whatsapp_group_url" text;
\ No newline at end of file
diff --git a/drizzle/meta/0017_snapshot.json b/drizzle/meta/0017_snapshot.json
new file mode 100644
index 0000000..4abb8e0
--- /dev/null
+++ b/drizzle/meta/0017_snapshot.json
@@ -0,0 +1,3360 @@
+{
+ "id": "fe5fd5df-ba3f-4bfc-ae94-84414258c2b5",
+ "prevId": "8d363aa4-7c0d-422a-9682-3623c829ca2e",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.accounting_categories": {
+ "name": "accounting_categories",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "accounting_category_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "accounting_categories_type_active_idx": {
+ "name": "accounting_categories_type_active_idx",
+ "columns": [
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "is_active",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.accounting_entries": {
+ "name": "accounting_entries",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "type": {
+ "name": "type",
+ "type": "accounting_entry_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "accounting_entry_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'draft'"
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "category_id": {
+ "name": "category_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "event_id": {
+ "name": "event_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "label": {
+ "name": "label",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "amount_cents": {
+ "name": "amount_cents",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "occurred_at": {
+ "name": "occurred_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "counterparty": {
+ "name": "counterparty",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "payment_method": {
+ "name": "payment_method",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reference": {
+ "name": "reference",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "notes": {
+ "name": "notes",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "attachment_url": {
+ "name": "attachment_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "version": {
+ "name": "version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "accounting_entries_occurred_at_idx": {
+ "name": "accounting_entries_occurred_at_idx",
+ "columns": [
+ {
+ "expression": "occurred_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "accounting_entries_status_occurred_at_idx": {
+ "name": "accounting_entries_status_occurred_at_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "occurred_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "accounting_entries_account_idx": {
+ "name": "accounting_entries_account_idx",
+ "columns": [
+ {
+ "expression": "account_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "accounting_entries_category_idx": {
+ "name": "accounting_entries_category_idx",
+ "columns": [
+ {
+ "expression": "category_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "accounting_entries_event_idx": {
+ "name": "accounting_entries_event_idx",
+ "columns": [
+ {
+ "expression": "event_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "accounting_entries_account_id_financial_accounts_id_fk": {
+ "name": "accounting_entries_account_id_financial_accounts_id_fk",
+ "tableFrom": "accounting_entries",
+ "tableTo": "financial_accounts",
+ "columnsFrom": [
+ "account_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "accounting_entries_category_id_accounting_categories_id_fk": {
+ "name": "accounting_entries_category_id_accounting_categories_id_fk",
+ "tableFrom": "accounting_entries",
+ "tableTo": "accounting_categories",
+ "columnsFrom": [
+ "category_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "accounting_entries_event_id_events_id_fk": {
+ "name": "accounting_entries_event_id_events_id_fk",
+ "tableFrom": "accounting_entries",
+ "tableTo": "events",
+ "columnsFrom": [
+ "event_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "accounting_entries_created_by_users_id_fk": {
+ "name": "accounting_entries_created_by_users_id_fk",
+ "tableFrom": "accounting_entries",
+ "tableTo": "users",
+ "columnsFrom": [
+ "created_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "accounting_entries_amount_cents_check": {
+ "name": "accounting_entries_amount_cents_check",
+ "value": "\"accounting_entries\".\"amount_cents\" > 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.association_documents": {
+ "name": "association_documents",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "type": {
+ "name": "type",
+ "type": "association_document_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "association_document_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'draft'"
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "document_date": {
+ "name": "document_date",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "member_id": {
+ "name": "member_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "file_url": {
+ "name": "file_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "version": {
+ "name": "version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "association_documents_type_status_date_idx": {
+ "name": "association_documents_type_status_date_idx",
+ "columns": [
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "document_date",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "association_documents_member_idx": {
+ "name": "association_documents_member_idx",
+ "columns": [
+ {
+ "expression": "member_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "association_documents_member_id_association_members_id_fk": {
+ "name": "association_documents_member_id_association_members_id_fk",
+ "tableFrom": "association_documents",
+ "tableTo": "association_members",
+ "columnsFrom": [
+ "member_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "association_documents_created_by_users_id_fk": {
+ "name": "association_documents_created_by_users_id_fk",
+ "tableFrom": "association_documents",
+ "tableTo": "users",
+ "columnsFrom": [
+ "created_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.association_members": {
+ "name": "association_members",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "first_name": {
+ "name": "first_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "last_name": {
+ "name": "last_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "phone": {
+ "name": "phone",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "address_line_1": {
+ "name": "address_line_1",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "address_line_2": {
+ "name": "address_line_2",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "postal_code": {
+ "name": "postal_code",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "city": {
+ "name": "city",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "country": {
+ "name": "country",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'France'"
+ },
+ "status": {
+ "name": "status",
+ "type": "association_member_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "school_year": {
+ "name": "school_year",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "membership_fee_cents": {
+ "name": "membership_fee_cents",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "fee_paid_at": {
+ "name": "fee_paid_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "joined_at": {
+ "name": "joined_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "notes": {
+ "name": "notes",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "version": {
+ "name": "version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "association_members_user_idx": {
+ "name": "association_members_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"association_members\".\"user_id\" is not null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "association_members_status_school_year_idx": {
+ "name": "association_members_status_school_year_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "school_year",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "association_members_name_idx": {
+ "name": "association_members_name_idx",
+ "columns": [
+ {
+ "expression": "last_name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "first_name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "association_members_user_id_users_id_fk": {
+ "name": "association_members_user_id_users_id_fk",
+ "tableFrom": "association_members",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "association_members_membership_fee_cents_check": {
+ "name": "association_members_membership_fee_cents_check",
+ "value": "\"association_members\".\"membership_fee_cents\" >= 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.association_settings": {
+ "name": "association_settings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "'default'"
+ },
+ "association_name": {
+ "name": "association_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'APEL Manager'"
+ },
+ "school_name": {
+ "name": "school_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'Votre établissement'"
+ },
+ "contact_email": {
+ "name": "contact_email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rna": {
+ "name": "rna",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "logo_url": {
+ "name": "logo_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "task_reminder_window_days": {
+ "name": "task_reminder_window_days",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 3
+ },
+ "volunteer_reminder_window_days": {
+ "name": "volunteer_reminder_window_days",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 2
+ },
+ "telegram_enabled": {
+ "name": "telegram_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "encrypted_telegram_bot_token": {
+ "name": "encrypted_telegram_bot_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "telegram_token_last_four": {
+ "name": "telegram_token_last_four",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "telegram_bot_username": {
+ "name": "telegram_bot_username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "telegram_token_verified_at": {
+ "name": "telegram_token_verified_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "recaptcha_enabled": {
+ "name": "recaptcha_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "recaptcha_site_key": {
+ "name": "recaptcha_site_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "encrypted_recaptcha_secret": {
+ "name": "encrypted_recaptcha_secret",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "recaptcha_min_score": {
+ "name": "recaptcha_min_score",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 50
+ },
+ "push_vapid_public_key": {
+ "name": "push_vapid_public_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "encrypted_push_vapid_private_key": {
+ "name": "encrypted_push_vapid_private_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_by": {
+ "name": "updated_by",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "association_settings_updated_by_users_id_fk": {
+ "name": "association_settings_updated_by_users_id_fk",
+ "tableFrom": "association_settings",
+ "tableTo": "users",
+ "columnsFrom": [
+ "updated_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "association_settings_singleton_check": {
+ "name": "association_settings_singleton_check",
+ "value": "\"association_settings\".\"id\" = 'default'"
+ },
+ "association_settings_task_reminder_window_check": {
+ "name": "association_settings_task_reminder_window_check",
+ "value": "\"association_settings\".\"task_reminder_window_days\" >= 0 and \"association_settings\".\"task_reminder_window_days\" <= 30"
+ },
+ "association_settings_recaptcha_min_score_check": {
+ "name": "association_settings_recaptcha_min_score_check",
+ "value": "\"association_settings\".\"recaptcha_min_score\" >= 0 and \"association_settings\".\"recaptcha_min_score\" <= 100"
+ },
+ "association_settings_volunteer_reminder_window_check": {
+ "name": "association_settings_volunteer_reminder_window_check",
+ "value": "\"association_settings\".\"volunteer_reminder_window_days\" >= 0 and \"association_settings\".\"volunteer_reminder_window_days\" <= 30"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.audit_logs": {
+ "name": "audit_logs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "actor_user_id": {
+ "name": "actor_user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "oauth_client_id": {
+ "name": "oauth_client_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "action": {
+ "name": "action",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entity_type": {
+ "name": "entity_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entity_id": {
+ "name": "entity_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source": {
+ "name": "source",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'web'"
+ },
+ "ip_address": {
+ "name": "ip_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "details": {
+ "name": "details",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "audit_logs_entity_idx": {
+ "name": "audit_logs_entity_idx",
+ "columns": [
+ {
+ "expression": "entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "audit_logs_actor_created_idx": {
+ "name": "audit_logs_actor_created_idx",
+ "columns": [
+ {
+ "expression": "actor_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "audit_logs_client_created_idx": {
+ "name": "audit_logs_client_created_idx",
+ "columns": [
+ {
+ "expression": "oauth_client_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "audit_logs_created_at_idx": {
+ "name": "audit_logs_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "audit_logs_actor_user_id_users_id_fk": {
+ "name": "audit_logs_actor_user_id_users_id_fk",
+ "tableFrom": "audit_logs",
+ "tableTo": "users",
+ "columnsFrom": [
+ "actor_user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "audit_logs_oauth_client_id_oauth_clients_id_fk": {
+ "name": "audit_logs_oauth_client_id_oauth_clients_id_fk",
+ "tableFrom": "audit_logs",
+ "tableTo": "oauth_clients",
+ "columnsFrom": [
+ "oauth_client_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.checklist_templates": {
+ "name": "checklist_templates",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tasks": {
+ "name": "tasks",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "version": {
+ "name": "version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.event_attachments": {
+ "name": "event_attachments",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "event_id": {
+ "name": "event_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "label": {
+ "name": "label",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_url": {
+ "name": "file_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "uploaded_by": {
+ "name": "uploaded_by",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "event_attachments_event_idx": {
+ "name": "event_attachments_event_idx",
+ "columns": [
+ {
+ "expression": "event_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "event_attachments_event_id_events_id_fk": {
+ "name": "event_attachments_event_id_events_id_fk",
+ "tableFrom": "event_attachments",
+ "tableTo": "events",
+ "columnsFrom": [
+ "event_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "event_attachments_uploaded_by_users_id_fk": {
+ "name": "event_attachments_uploaded_by_users_id_fk",
+ "tableFrom": "event_attachments",
+ "tableTo": "users",
+ "columnsFrom": [
+ "uploaded_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.events": {
+ "name": "events",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "public_description": {
+ "name": "public_description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ticketing_url": {
+ "name": "ticketing_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "location": {
+ "name": "location",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "start_at": {
+ "name": "start_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "end_at": {
+ "name": "end_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "event_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'draft'"
+ },
+ "version": {
+ "name": "version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "share_token": {
+ "name": "share_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "events_status_start_idx": {
+ "name": "events_status_start_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "start_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "events_created_by_users_id_fk": {
+ "name": "events_created_by_users_id_fk",
+ "tableFrom": "events",
+ "tableTo": "users",
+ "columnsFrom": [
+ "created_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "events_share_token_unique": {
+ "name": "events_share_token_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "share_token"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.financial_accounts": {
+ "name": "financial_accounts",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "financial_account_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "financial_accounts_type_active_idx": {
+ "name": "financial_accounts_type_active_idx",
+ "columns": [
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "is_active",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.notifications_log": {
+ "name": "notifications_log",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "task_id": {
+ "name": "task_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "kind": {
+ "name": "kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "sent_at": {
+ "name": "sent_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "notifications_log_task_user_kind_idx": {
+ "name": "notifications_log_task_user_kind_idx",
+ "columns": [
+ {
+ "expression": "task_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "kind",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "notifications_log_task_id_tasks_id_fk": {
+ "name": "notifications_log_task_id_tasks_id_fk",
+ "tableFrom": "notifications_log",
+ "tableTo": "tasks",
+ "columnsFrom": [
+ "task_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "notifications_log_user_id_users_id_fk": {
+ "name": "notifications_log_user_id_users_id_fk",
+ "tableFrom": "notifications_log",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.oauth_authorization_codes": {
+ "name": "oauth_authorization_codes",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "code_hash": {
+ "name": "code_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "oauth_client_id": {
+ "name": "oauth_client_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "redirect_uri": {
+ "name": "redirect_uri",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "scopes": {
+ "name": "scopes",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "code_challenge": {
+ "name": "code_challenge",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "code_challenge_method": {
+ "name": "code_challenge_method",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'S256'"
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "used_at": {
+ "name": "used_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "oauth_authorization_codes_code_hash_idx": {
+ "name": "oauth_authorization_codes_code_hash_idx",
+ "columns": [
+ {
+ "expression": "code_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oauth_authorization_codes_client_expires_idx": {
+ "name": "oauth_authorization_codes_client_expires_idx",
+ "columns": [
+ {
+ "expression": "oauth_client_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oauth_authorization_codes_user_idx": {
+ "name": "oauth_authorization_codes_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "oauth_authorization_codes_oauth_client_id_oauth_clients_id_fk": {
+ "name": "oauth_authorization_codes_oauth_client_id_oauth_clients_id_fk",
+ "tableFrom": "oauth_authorization_codes",
+ "tableTo": "oauth_clients",
+ "columnsFrom": [
+ "oauth_client_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "oauth_authorization_codes_user_id_users_id_fk": {
+ "name": "oauth_authorization_codes_user_id_users_id_fk",
+ "tableFrom": "oauth_authorization_codes",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.oauth_clients": {
+ "name": "oauth_clients",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "client_id": {
+ "name": "client_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "client_secret_hash": {
+ "name": "client_secret_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "redirect_uris": {
+ "name": "redirect_uris",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "token_endpoint_auth_method": {
+ "name": "token_endpoint_auth_method",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'none'"
+ },
+ "grant_types": {
+ "name": "grant_types",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[\"authorization_code\",\"refresh_token\"]'::jsonb"
+ },
+ "scopes": {
+ "name": "scopes",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "oauth_clients_client_id_idx": {
+ "name": "oauth_clients_client_id_idx",
+ "columns": [
+ {
+ "expression": "client_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oauth_clients_enabled_idx": {
+ "name": "oauth_clients_enabled_idx",
+ "columns": [
+ {
+ "expression": "enabled",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "oauth_clients_created_by_users_id_fk": {
+ "name": "oauth_clients_created_by_users_id_fk",
+ "tableFrom": "oauth_clients",
+ "tableTo": "users",
+ "columnsFrom": [
+ "created_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.oauth_tokens": {
+ "name": "oauth_tokens",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "token_hash": {
+ "name": "token_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "oauth_token_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "oauth_client_id": {
+ "name": "oauth_client_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "authorization_code_id": {
+ "name": "authorization_code_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scopes": {
+ "name": "scopes",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "revoked_at": {
+ "name": "revoked_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "oauth_tokens_token_hash_idx": {
+ "name": "oauth_tokens_token_hash_idx",
+ "columns": [
+ {
+ "expression": "token_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oauth_tokens_client_type_expires_idx": {
+ "name": "oauth_tokens_client_type_expires_idx",
+ "columns": [
+ {
+ "expression": "oauth_client_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oauth_tokens_user_idx": {
+ "name": "oauth_tokens_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oauth_tokens_authorization_code_idx": {
+ "name": "oauth_tokens_authorization_code_idx",
+ "columns": [
+ {
+ "expression": "authorization_code_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "oauth_tokens_oauth_client_id_oauth_clients_id_fk": {
+ "name": "oauth_tokens_oauth_client_id_oauth_clients_id_fk",
+ "tableFrom": "oauth_tokens",
+ "tableTo": "oauth_clients",
+ "columnsFrom": [
+ "oauth_client_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "oauth_tokens_user_id_users_id_fk": {
+ "name": "oauth_tokens_user_id_users_id_fk",
+ "tableFrom": "oauth_tokens",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "oauth_tokens_authorization_code_id_oauth_authorization_codes_id_fk": {
+ "name": "oauth_tokens_authorization_code_id_oauth_authorization_codes_id_fk",
+ "tableFrom": "oauth_tokens",
+ "tableTo": "oauth_authorization_codes",
+ "columnsFrom": [
+ "authorization_code_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.outbound_mail_settings": {
+ "name": "outbound_mail_settings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "'default'"
+ },
+ "provider": {
+ "name": "provider",
+ "type": "outbound_mail_provider",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'resend'"
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "from_name": {
+ "name": "from_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "from_email": {
+ "name": "from_email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reply_to": {
+ "name": "reply_to",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "domain": {
+ "name": "domain",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "encrypted_api_key": {
+ "name": "encrypted_api_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "key_last_four": {
+ "name": "key_last_four",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "smtp_host": {
+ "name": "smtp_host",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "smtp_port": {
+ "name": "smtp_port",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "smtp_secure": {
+ "name": "smtp_secure",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "smtp_username": {
+ "name": "smtp_username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "encrypted_smtp_password": {
+ "name": "encrypted_smtp_password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_tested_at": {
+ "name": "last_tested_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_test_status": {
+ "name": "last_test_status",
+ "type": "outbound_mail_test_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'untested'"
+ },
+ "updated_by": {
+ "name": "updated_by",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "outbound_mail_settings_updated_by_users_id_fk": {
+ "name": "outbound_mail_settings_updated_by_users_id_fk",
+ "tableFrom": "outbound_mail_settings",
+ "tableTo": "users",
+ "columnsFrom": [
+ "updated_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "outbound_mail_settings_singleton_check": {
+ "name": "outbound_mail_settings_singleton_check",
+ "value": "\"outbound_mail_settings\".\"id\" = 'default'"
+ },
+ "outbound_mail_settings_smtp_port_check": {
+ "name": "outbound_mail_settings_smtp_port_check",
+ "value": "\"outbound_mail_settings\".\"smtp_port\" is null or (\"outbound_mail_settings\".\"smtp_port\" >= 1 and \"outbound_mail_settings\".\"smtp_port\" <= 65535)"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.password_reset_tokens": {
+ "name": "password_reset_tokens",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token_hash": {
+ "name": "token_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "used_at": {
+ "name": "used_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "password_reset_tokens_user_id_users_id_fk": {
+ "name": "password_reset_tokens_user_id_users_id_fk",
+ "tableFrom": "password_reset_tokens",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.push_deliveries": {
+ "name": "push_deliveries",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "notification_id": {
+ "name": "notification_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "subscription_id": {
+ "name": "subscription_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "push_delivery_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'queued'"
+ },
+ "ack_token": {
+ "name": "ack_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "error": {
+ "name": "error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sent_at": {
+ "name": "sent_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "received_at": {
+ "name": "received_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "opened_at": {
+ "name": "opened_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "push_deliveries_notification_idx": {
+ "name": "push_deliveries_notification_idx",
+ "columns": [
+ {
+ "expression": "notification_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "push_deliveries_ack_token_idx": {
+ "name": "push_deliveries_ack_token_idx",
+ "columns": [
+ {
+ "expression": "ack_token",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "push_deliveries_user_idx": {
+ "name": "push_deliveries_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "push_deliveries_notification_id_push_notifications_id_fk": {
+ "name": "push_deliveries_notification_id_push_notifications_id_fk",
+ "tableFrom": "push_deliveries",
+ "tableTo": "push_notifications",
+ "columnsFrom": [
+ "notification_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "push_deliveries_user_id_users_id_fk": {
+ "name": "push_deliveries_user_id_users_id_fk",
+ "tableFrom": "push_deliveries",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "push_deliveries_subscription_id_push_subscriptions_id_fk": {
+ "name": "push_deliveries_subscription_id_push_subscriptions_id_fk",
+ "tableFrom": "push_deliveries",
+ "tableTo": "push_subscriptions",
+ "columnsFrom": [
+ "subscription_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.push_notifications": {
+ "name": "push_notifications",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "sender_id": {
+ "name": "sender_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "body": {
+ "name": "body",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "push_notifications_created_at_idx": {
+ "name": "push_notifications_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "push_notifications_sender_id_users_id_fk": {
+ "name": "push_notifications_sender_id_users_id_fk",
+ "tableFrom": "push_notifications",
+ "tableTo": "users",
+ "columnsFrom": [
+ "sender_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.push_subscriptions": {
+ "name": "push_subscriptions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "endpoint": {
+ "name": "endpoint",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "p256dh": {
+ "name": "p256dh",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "auth": {
+ "name": "auth",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "device_label": {
+ "name": "device_label",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_success_at": {
+ "name": "last_success_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "push_subscriptions_endpoint_idx": {
+ "name": "push_subscriptions_endpoint_idx",
+ "columns": [
+ {
+ "expression": "endpoint",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "push_subscriptions_user_idx": {
+ "name": "push_subscriptions_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "push_subscriptions_user_id_users_id_fk": {
+ "name": "push_subscriptions_user_id_users_id_fk",
+ "tableFrom": "push_subscriptions",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.task_assignees": {
+ "name": "task_assignees",
+ "schema": "",
+ "columns": {
+ "task_id": {
+ "name": "task_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "task_assignees_user_idx": {
+ "name": "task_assignees_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "task_assignees_task_id_tasks_id_fk": {
+ "name": "task_assignees_task_id_tasks_id_fk",
+ "tableFrom": "task_assignees",
+ "tableTo": "tasks",
+ "columnsFrom": [
+ "task_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "task_assignees_user_id_users_id_fk": {
+ "name": "task_assignees_user_id_users_id_fk",
+ "tableFrom": "task_assignees",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "task_assignees_task_id_user_id_pk": {
+ "name": "task_assignees_task_id_user_id_pk",
+ "columns": [
+ "task_id",
+ "user_id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tasks": {
+ "name": "tasks",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "event_id": {
+ "name": "event_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "lead_time_days": {
+ "name": "lead_time_days",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 7
+ },
+ "lead_time_value": {
+ "name": "lead_time_value",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 7
+ },
+ "lead_time_unit": {
+ "name": "lead_time_unit",
+ "type": "task_lead_time_unit",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'days'"
+ },
+ "due_at": {
+ "name": "due_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "task_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'todo'"
+ },
+ "position": {
+ "name": "position",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "version": {
+ "name": "version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "tasks_event_idx": {
+ "name": "tasks_event_idx",
+ "columns": [
+ {
+ "expression": "event_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "tasks_status_due_idx": {
+ "name": "tasks_status_due_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "due_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "tasks_event_id_events_id_fk": {
+ "name": "tasks_event_id_events_id_fk",
+ "tableFrom": "tasks",
+ "tableTo": "events",
+ "columnsFrom": [
+ "event_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "tasks_lead_time_value_check": {
+ "name": "tasks_lead_time_value_check",
+ "value": "\"tasks\".\"lead_time_value\" >= 0"
+ },
+ "tasks_lead_time_consistency_check": {
+ "name": "tasks_lead_time_consistency_check",
+ "value": "\"tasks\".\"lead_time_days\" = \"tasks\".\"lead_time_value\" * case \"tasks\".\"lead_time_unit\" when 'days' then 1 when 'weeks' then 7 when 'months' then 30 end"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.users": {
+ "name": "users",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "password_hash": {
+ "name": "password_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "role",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'member'"
+ },
+ "telegram_chat_id": {
+ "name": "telegram_chat_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "session_epoch": {
+ "name": "session_epoch",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "onboarding_seen_at": {
+ "name": "onboarding_seen_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "push_enabled": {
+ "name": "push_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "users_email_unique": {
+ "name": "users_email_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "email"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.volunteer_signups": {
+ "name": "volunteer_signups",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "slot_id": {
+ "name": "slot_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "phone": {
+ "name": "phone",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cancel_token": {
+ "name": "cancel_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reminded_at": {
+ "name": "reminded_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "volunteer_signups_slot_idx": {
+ "name": "volunteer_signups_slot_idx",
+ "columns": [
+ {
+ "expression": "slot_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "volunteer_signups_user_idx": {
+ "name": "volunteer_signups_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "volunteer_signups_cancel_token_idx": {
+ "name": "volunteer_signups_cancel_token_idx",
+ "columns": [
+ {
+ "expression": "cancel_token",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "volunteer_signups_slot_email_idx": {
+ "name": "volunteer_signups_slot_email_idx",
+ "columns": [
+ {
+ "expression": "slot_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "lower(\"email\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"volunteer_signups\".\"email\" is not null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "volunteer_signups_slot_id_volunteer_slots_id_fk": {
+ "name": "volunteer_signups_slot_id_volunteer_slots_id_fk",
+ "tableFrom": "volunteer_signups",
+ "tableTo": "volunteer_slots",
+ "columnsFrom": [
+ "slot_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "volunteer_signups_user_id_users_id_fk": {
+ "name": "volunteer_signups_user_id_users_id_fk",
+ "tableFrom": "volunteer_signups",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.volunteer_slots": {
+ "name": "volunteer_slots",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "event_id": {
+ "name": "event_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "start_at": {
+ "name": "start_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "end_at": {
+ "name": "end_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "capacity": {
+ "name": "capacity",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "volunteer_slots_event_idx": {
+ "name": "volunteer_slots_event_idx",
+ "columns": [
+ {
+ "expression": "event_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "volunteer_slots_event_id_events_id_fk": {
+ "name": "volunteer_slots_event_id_events_id_fk",
+ "tableFrom": "volunteer_slots",
+ "tableTo": "events",
+ "columnsFrom": [
+ "event_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {
+ "public.accounting_category_type": {
+ "name": "accounting_category_type",
+ "schema": "public",
+ "values": [
+ "income",
+ "expense"
+ ]
+ },
+ "public.accounting_entry_status": {
+ "name": "accounting_entry_status",
+ "schema": "public",
+ "values": [
+ "draft",
+ "posted"
+ ]
+ },
+ "public.accounting_entry_type": {
+ "name": "accounting_entry_type",
+ "schema": "public",
+ "values": [
+ "income",
+ "expense"
+ ]
+ },
+ "public.association_document_status": {
+ "name": "association_document_status",
+ "schema": "public",
+ "values": [
+ "draft",
+ "final",
+ "archived"
+ ]
+ },
+ "public.association_document_type": {
+ "name": "association_document_type",
+ "schema": "public",
+ "values": [
+ "ag_minutes",
+ "attestation",
+ "statutes",
+ "internal_rules",
+ "insurance",
+ "agreement",
+ "other"
+ ]
+ },
+ "public.association_member_status": {
+ "name": "association_member_status",
+ "schema": "public",
+ "values": [
+ "active",
+ "pending",
+ "inactive"
+ ]
+ },
+ "public.event_status": {
+ "name": "event_status",
+ "schema": "public",
+ "values": [
+ "draft",
+ "published",
+ "archived"
+ ]
+ },
+ "public.financial_account_type": {
+ "name": "financial_account_type",
+ "schema": "public",
+ "values": [
+ "bank",
+ "cash"
+ ]
+ },
+ "public.oauth_token_type": {
+ "name": "oauth_token_type",
+ "schema": "public",
+ "values": [
+ "access",
+ "refresh"
+ ]
+ },
+ "public.outbound_mail_provider": {
+ "name": "outbound_mail_provider",
+ "schema": "public",
+ "values": [
+ "resend",
+ "smtp"
+ ]
+ },
+ "public.outbound_mail_test_status": {
+ "name": "outbound_mail_test_status",
+ "schema": "public",
+ "values": [
+ "untested",
+ "success",
+ "failed"
+ ]
+ },
+ "public.push_delivery_status": {
+ "name": "push_delivery_status",
+ "schema": "public",
+ "values": [
+ "queued",
+ "sent",
+ "failed",
+ "received",
+ "opened"
+ ]
+ },
+ "public.role": {
+ "name": "role",
+ "schema": "public",
+ "values": [
+ "admin",
+ "manager",
+ "member"
+ ]
+ },
+ "public.task_lead_time_unit": {
+ "name": "task_lead_time_unit",
+ "schema": "public",
+ "values": [
+ "days",
+ "weeks",
+ "months"
+ ]
+ },
+ "public.task_status": {
+ "name": "task_status",
+ "schema": "public",
+ "values": [
+ "todo",
+ "in_progress",
+ "done"
+ ]
+ }
+ },
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
\ No newline at end of file
diff --git a/drizzle/meta/0018_snapshot.json b/drizzle/meta/0018_snapshot.json
new file mode 100644
index 0000000..fa5d0d9
--- /dev/null
+++ b/drizzle/meta/0018_snapshot.json
@@ -0,0 +1,3366 @@
+{
+ "id": "f9a7079b-09d7-4a8f-a891-77d426f49664",
+ "prevId": "fe5fd5df-ba3f-4bfc-ae94-84414258c2b5",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.accounting_categories": {
+ "name": "accounting_categories",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "accounting_category_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "accounting_categories_type_active_idx": {
+ "name": "accounting_categories_type_active_idx",
+ "columns": [
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "is_active",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.accounting_entries": {
+ "name": "accounting_entries",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "type": {
+ "name": "type",
+ "type": "accounting_entry_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "accounting_entry_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'draft'"
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "category_id": {
+ "name": "category_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "event_id": {
+ "name": "event_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "label": {
+ "name": "label",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "amount_cents": {
+ "name": "amount_cents",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "occurred_at": {
+ "name": "occurred_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "counterparty": {
+ "name": "counterparty",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "payment_method": {
+ "name": "payment_method",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reference": {
+ "name": "reference",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "notes": {
+ "name": "notes",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "attachment_url": {
+ "name": "attachment_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "version": {
+ "name": "version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "accounting_entries_occurred_at_idx": {
+ "name": "accounting_entries_occurred_at_idx",
+ "columns": [
+ {
+ "expression": "occurred_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "accounting_entries_status_occurred_at_idx": {
+ "name": "accounting_entries_status_occurred_at_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "occurred_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "accounting_entries_account_idx": {
+ "name": "accounting_entries_account_idx",
+ "columns": [
+ {
+ "expression": "account_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "accounting_entries_category_idx": {
+ "name": "accounting_entries_category_idx",
+ "columns": [
+ {
+ "expression": "category_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "accounting_entries_event_idx": {
+ "name": "accounting_entries_event_idx",
+ "columns": [
+ {
+ "expression": "event_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "accounting_entries_account_id_financial_accounts_id_fk": {
+ "name": "accounting_entries_account_id_financial_accounts_id_fk",
+ "tableFrom": "accounting_entries",
+ "tableTo": "financial_accounts",
+ "columnsFrom": [
+ "account_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "accounting_entries_category_id_accounting_categories_id_fk": {
+ "name": "accounting_entries_category_id_accounting_categories_id_fk",
+ "tableFrom": "accounting_entries",
+ "tableTo": "accounting_categories",
+ "columnsFrom": [
+ "category_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "accounting_entries_event_id_events_id_fk": {
+ "name": "accounting_entries_event_id_events_id_fk",
+ "tableFrom": "accounting_entries",
+ "tableTo": "events",
+ "columnsFrom": [
+ "event_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "accounting_entries_created_by_users_id_fk": {
+ "name": "accounting_entries_created_by_users_id_fk",
+ "tableFrom": "accounting_entries",
+ "tableTo": "users",
+ "columnsFrom": [
+ "created_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "accounting_entries_amount_cents_check": {
+ "name": "accounting_entries_amount_cents_check",
+ "value": "\"accounting_entries\".\"amount_cents\" > 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.association_documents": {
+ "name": "association_documents",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "type": {
+ "name": "type",
+ "type": "association_document_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "association_document_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'draft'"
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "document_date": {
+ "name": "document_date",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "member_id": {
+ "name": "member_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "file_url": {
+ "name": "file_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "version": {
+ "name": "version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "association_documents_type_status_date_idx": {
+ "name": "association_documents_type_status_date_idx",
+ "columns": [
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "document_date",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "association_documents_member_idx": {
+ "name": "association_documents_member_idx",
+ "columns": [
+ {
+ "expression": "member_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "association_documents_member_id_association_members_id_fk": {
+ "name": "association_documents_member_id_association_members_id_fk",
+ "tableFrom": "association_documents",
+ "tableTo": "association_members",
+ "columnsFrom": [
+ "member_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "association_documents_created_by_users_id_fk": {
+ "name": "association_documents_created_by_users_id_fk",
+ "tableFrom": "association_documents",
+ "tableTo": "users",
+ "columnsFrom": [
+ "created_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.association_members": {
+ "name": "association_members",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "first_name": {
+ "name": "first_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "last_name": {
+ "name": "last_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "phone": {
+ "name": "phone",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "address_line_1": {
+ "name": "address_line_1",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "address_line_2": {
+ "name": "address_line_2",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "postal_code": {
+ "name": "postal_code",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "city": {
+ "name": "city",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "country": {
+ "name": "country",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'France'"
+ },
+ "status": {
+ "name": "status",
+ "type": "association_member_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "school_year": {
+ "name": "school_year",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "membership_fee_cents": {
+ "name": "membership_fee_cents",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "fee_paid_at": {
+ "name": "fee_paid_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "joined_at": {
+ "name": "joined_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "notes": {
+ "name": "notes",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "version": {
+ "name": "version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "association_members_user_idx": {
+ "name": "association_members_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"association_members\".\"user_id\" is not null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "association_members_status_school_year_idx": {
+ "name": "association_members_status_school_year_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "school_year",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "association_members_name_idx": {
+ "name": "association_members_name_idx",
+ "columns": [
+ {
+ "expression": "last_name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "first_name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "association_members_user_id_users_id_fk": {
+ "name": "association_members_user_id_users_id_fk",
+ "tableFrom": "association_members",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "association_members_membership_fee_cents_check": {
+ "name": "association_members_membership_fee_cents_check",
+ "value": "\"association_members\".\"membership_fee_cents\" >= 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.association_settings": {
+ "name": "association_settings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "'default'"
+ },
+ "association_name": {
+ "name": "association_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'APEL Manager'"
+ },
+ "school_name": {
+ "name": "school_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'Votre établissement'"
+ },
+ "contact_email": {
+ "name": "contact_email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rna": {
+ "name": "rna",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "logo_url": {
+ "name": "logo_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "task_reminder_window_days": {
+ "name": "task_reminder_window_days",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 3
+ },
+ "volunteer_reminder_window_days": {
+ "name": "volunteer_reminder_window_days",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 2
+ },
+ "telegram_enabled": {
+ "name": "telegram_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "encrypted_telegram_bot_token": {
+ "name": "encrypted_telegram_bot_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "telegram_token_last_four": {
+ "name": "telegram_token_last_four",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "telegram_bot_username": {
+ "name": "telegram_bot_username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "telegram_token_verified_at": {
+ "name": "telegram_token_verified_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "recaptcha_enabled": {
+ "name": "recaptcha_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "recaptcha_site_key": {
+ "name": "recaptcha_site_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "encrypted_recaptcha_secret": {
+ "name": "encrypted_recaptcha_secret",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "recaptcha_min_score": {
+ "name": "recaptcha_min_score",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 50
+ },
+ "whatsapp_group_url": {
+ "name": "whatsapp_group_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "push_vapid_public_key": {
+ "name": "push_vapid_public_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "encrypted_push_vapid_private_key": {
+ "name": "encrypted_push_vapid_private_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_by": {
+ "name": "updated_by",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "association_settings_updated_by_users_id_fk": {
+ "name": "association_settings_updated_by_users_id_fk",
+ "tableFrom": "association_settings",
+ "tableTo": "users",
+ "columnsFrom": [
+ "updated_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "association_settings_singleton_check": {
+ "name": "association_settings_singleton_check",
+ "value": "\"association_settings\".\"id\" = 'default'"
+ },
+ "association_settings_task_reminder_window_check": {
+ "name": "association_settings_task_reminder_window_check",
+ "value": "\"association_settings\".\"task_reminder_window_days\" >= 0 and \"association_settings\".\"task_reminder_window_days\" <= 30"
+ },
+ "association_settings_recaptcha_min_score_check": {
+ "name": "association_settings_recaptcha_min_score_check",
+ "value": "\"association_settings\".\"recaptcha_min_score\" >= 0 and \"association_settings\".\"recaptcha_min_score\" <= 100"
+ },
+ "association_settings_volunteer_reminder_window_check": {
+ "name": "association_settings_volunteer_reminder_window_check",
+ "value": "\"association_settings\".\"volunteer_reminder_window_days\" >= 0 and \"association_settings\".\"volunteer_reminder_window_days\" <= 30"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.audit_logs": {
+ "name": "audit_logs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "actor_user_id": {
+ "name": "actor_user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "oauth_client_id": {
+ "name": "oauth_client_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "action": {
+ "name": "action",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entity_type": {
+ "name": "entity_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entity_id": {
+ "name": "entity_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source": {
+ "name": "source",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'web'"
+ },
+ "ip_address": {
+ "name": "ip_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "details": {
+ "name": "details",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "audit_logs_entity_idx": {
+ "name": "audit_logs_entity_idx",
+ "columns": [
+ {
+ "expression": "entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "audit_logs_actor_created_idx": {
+ "name": "audit_logs_actor_created_idx",
+ "columns": [
+ {
+ "expression": "actor_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "audit_logs_client_created_idx": {
+ "name": "audit_logs_client_created_idx",
+ "columns": [
+ {
+ "expression": "oauth_client_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "audit_logs_created_at_idx": {
+ "name": "audit_logs_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "audit_logs_actor_user_id_users_id_fk": {
+ "name": "audit_logs_actor_user_id_users_id_fk",
+ "tableFrom": "audit_logs",
+ "tableTo": "users",
+ "columnsFrom": [
+ "actor_user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "audit_logs_oauth_client_id_oauth_clients_id_fk": {
+ "name": "audit_logs_oauth_client_id_oauth_clients_id_fk",
+ "tableFrom": "audit_logs",
+ "tableTo": "oauth_clients",
+ "columnsFrom": [
+ "oauth_client_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.checklist_templates": {
+ "name": "checklist_templates",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tasks": {
+ "name": "tasks",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "version": {
+ "name": "version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.event_attachments": {
+ "name": "event_attachments",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "event_id": {
+ "name": "event_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "label": {
+ "name": "label",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_url": {
+ "name": "file_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "uploaded_by": {
+ "name": "uploaded_by",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "event_attachments_event_idx": {
+ "name": "event_attachments_event_idx",
+ "columns": [
+ {
+ "expression": "event_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "event_attachments_event_id_events_id_fk": {
+ "name": "event_attachments_event_id_events_id_fk",
+ "tableFrom": "event_attachments",
+ "tableTo": "events",
+ "columnsFrom": [
+ "event_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "event_attachments_uploaded_by_users_id_fk": {
+ "name": "event_attachments_uploaded_by_users_id_fk",
+ "tableFrom": "event_attachments",
+ "tableTo": "users",
+ "columnsFrom": [
+ "uploaded_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.events": {
+ "name": "events",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "public_description": {
+ "name": "public_description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ticketing_url": {
+ "name": "ticketing_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "location": {
+ "name": "location",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "start_at": {
+ "name": "start_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "end_at": {
+ "name": "end_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "event_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'draft'"
+ },
+ "version": {
+ "name": "version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "share_token": {
+ "name": "share_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "events_status_start_idx": {
+ "name": "events_status_start_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "start_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "events_created_by_users_id_fk": {
+ "name": "events_created_by_users_id_fk",
+ "tableFrom": "events",
+ "tableTo": "users",
+ "columnsFrom": [
+ "created_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "events_share_token_unique": {
+ "name": "events_share_token_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "share_token"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.financial_accounts": {
+ "name": "financial_accounts",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "financial_account_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "financial_accounts_type_active_idx": {
+ "name": "financial_accounts_type_active_idx",
+ "columns": [
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "is_active",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.notifications_log": {
+ "name": "notifications_log",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "task_id": {
+ "name": "task_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "kind": {
+ "name": "kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "sent_at": {
+ "name": "sent_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "notifications_log_task_user_kind_idx": {
+ "name": "notifications_log_task_user_kind_idx",
+ "columns": [
+ {
+ "expression": "task_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "kind",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "notifications_log_task_id_tasks_id_fk": {
+ "name": "notifications_log_task_id_tasks_id_fk",
+ "tableFrom": "notifications_log",
+ "tableTo": "tasks",
+ "columnsFrom": [
+ "task_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "notifications_log_user_id_users_id_fk": {
+ "name": "notifications_log_user_id_users_id_fk",
+ "tableFrom": "notifications_log",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.oauth_authorization_codes": {
+ "name": "oauth_authorization_codes",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "code_hash": {
+ "name": "code_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "oauth_client_id": {
+ "name": "oauth_client_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "redirect_uri": {
+ "name": "redirect_uri",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "scopes": {
+ "name": "scopes",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "code_challenge": {
+ "name": "code_challenge",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "code_challenge_method": {
+ "name": "code_challenge_method",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'S256'"
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "used_at": {
+ "name": "used_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "oauth_authorization_codes_code_hash_idx": {
+ "name": "oauth_authorization_codes_code_hash_idx",
+ "columns": [
+ {
+ "expression": "code_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oauth_authorization_codes_client_expires_idx": {
+ "name": "oauth_authorization_codes_client_expires_idx",
+ "columns": [
+ {
+ "expression": "oauth_client_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oauth_authorization_codes_user_idx": {
+ "name": "oauth_authorization_codes_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "oauth_authorization_codes_oauth_client_id_oauth_clients_id_fk": {
+ "name": "oauth_authorization_codes_oauth_client_id_oauth_clients_id_fk",
+ "tableFrom": "oauth_authorization_codes",
+ "tableTo": "oauth_clients",
+ "columnsFrom": [
+ "oauth_client_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "oauth_authorization_codes_user_id_users_id_fk": {
+ "name": "oauth_authorization_codes_user_id_users_id_fk",
+ "tableFrom": "oauth_authorization_codes",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.oauth_clients": {
+ "name": "oauth_clients",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "client_id": {
+ "name": "client_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "client_secret_hash": {
+ "name": "client_secret_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "redirect_uris": {
+ "name": "redirect_uris",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "token_endpoint_auth_method": {
+ "name": "token_endpoint_auth_method",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'none'"
+ },
+ "grant_types": {
+ "name": "grant_types",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[\"authorization_code\",\"refresh_token\"]'::jsonb"
+ },
+ "scopes": {
+ "name": "scopes",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "oauth_clients_client_id_idx": {
+ "name": "oauth_clients_client_id_idx",
+ "columns": [
+ {
+ "expression": "client_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oauth_clients_enabled_idx": {
+ "name": "oauth_clients_enabled_idx",
+ "columns": [
+ {
+ "expression": "enabled",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "oauth_clients_created_by_users_id_fk": {
+ "name": "oauth_clients_created_by_users_id_fk",
+ "tableFrom": "oauth_clients",
+ "tableTo": "users",
+ "columnsFrom": [
+ "created_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.oauth_tokens": {
+ "name": "oauth_tokens",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "token_hash": {
+ "name": "token_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "oauth_token_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "oauth_client_id": {
+ "name": "oauth_client_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "authorization_code_id": {
+ "name": "authorization_code_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scopes": {
+ "name": "scopes",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "revoked_at": {
+ "name": "revoked_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "oauth_tokens_token_hash_idx": {
+ "name": "oauth_tokens_token_hash_idx",
+ "columns": [
+ {
+ "expression": "token_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oauth_tokens_client_type_expires_idx": {
+ "name": "oauth_tokens_client_type_expires_idx",
+ "columns": [
+ {
+ "expression": "oauth_client_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oauth_tokens_user_idx": {
+ "name": "oauth_tokens_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oauth_tokens_authorization_code_idx": {
+ "name": "oauth_tokens_authorization_code_idx",
+ "columns": [
+ {
+ "expression": "authorization_code_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "oauth_tokens_oauth_client_id_oauth_clients_id_fk": {
+ "name": "oauth_tokens_oauth_client_id_oauth_clients_id_fk",
+ "tableFrom": "oauth_tokens",
+ "tableTo": "oauth_clients",
+ "columnsFrom": [
+ "oauth_client_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "oauth_tokens_user_id_users_id_fk": {
+ "name": "oauth_tokens_user_id_users_id_fk",
+ "tableFrom": "oauth_tokens",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "oauth_tokens_authorization_code_id_oauth_authorization_codes_id_fk": {
+ "name": "oauth_tokens_authorization_code_id_oauth_authorization_codes_id_fk",
+ "tableFrom": "oauth_tokens",
+ "tableTo": "oauth_authorization_codes",
+ "columnsFrom": [
+ "authorization_code_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.outbound_mail_settings": {
+ "name": "outbound_mail_settings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "'default'"
+ },
+ "provider": {
+ "name": "provider",
+ "type": "outbound_mail_provider",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'resend'"
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "from_name": {
+ "name": "from_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "from_email": {
+ "name": "from_email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reply_to": {
+ "name": "reply_to",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "domain": {
+ "name": "domain",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "encrypted_api_key": {
+ "name": "encrypted_api_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "key_last_four": {
+ "name": "key_last_four",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "smtp_host": {
+ "name": "smtp_host",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "smtp_port": {
+ "name": "smtp_port",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "smtp_secure": {
+ "name": "smtp_secure",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "smtp_username": {
+ "name": "smtp_username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "encrypted_smtp_password": {
+ "name": "encrypted_smtp_password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_tested_at": {
+ "name": "last_tested_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_test_status": {
+ "name": "last_test_status",
+ "type": "outbound_mail_test_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'untested'"
+ },
+ "updated_by": {
+ "name": "updated_by",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "outbound_mail_settings_updated_by_users_id_fk": {
+ "name": "outbound_mail_settings_updated_by_users_id_fk",
+ "tableFrom": "outbound_mail_settings",
+ "tableTo": "users",
+ "columnsFrom": [
+ "updated_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "outbound_mail_settings_singleton_check": {
+ "name": "outbound_mail_settings_singleton_check",
+ "value": "\"outbound_mail_settings\".\"id\" = 'default'"
+ },
+ "outbound_mail_settings_smtp_port_check": {
+ "name": "outbound_mail_settings_smtp_port_check",
+ "value": "\"outbound_mail_settings\".\"smtp_port\" is null or (\"outbound_mail_settings\".\"smtp_port\" >= 1 and \"outbound_mail_settings\".\"smtp_port\" <= 65535)"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.password_reset_tokens": {
+ "name": "password_reset_tokens",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token_hash": {
+ "name": "token_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "used_at": {
+ "name": "used_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "password_reset_tokens_user_id_users_id_fk": {
+ "name": "password_reset_tokens_user_id_users_id_fk",
+ "tableFrom": "password_reset_tokens",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.push_deliveries": {
+ "name": "push_deliveries",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "notification_id": {
+ "name": "notification_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "subscription_id": {
+ "name": "subscription_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "push_delivery_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'queued'"
+ },
+ "ack_token": {
+ "name": "ack_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "error": {
+ "name": "error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sent_at": {
+ "name": "sent_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "received_at": {
+ "name": "received_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "opened_at": {
+ "name": "opened_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "push_deliveries_notification_idx": {
+ "name": "push_deliveries_notification_idx",
+ "columns": [
+ {
+ "expression": "notification_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "push_deliveries_ack_token_idx": {
+ "name": "push_deliveries_ack_token_idx",
+ "columns": [
+ {
+ "expression": "ack_token",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "push_deliveries_user_idx": {
+ "name": "push_deliveries_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "push_deliveries_notification_id_push_notifications_id_fk": {
+ "name": "push_deliveries_notification_id_push_notifications_id_fk",
+ "tableFrom": "push_deliveries",
+ "tableTo": "push_notifications",
+ "columnsFrom": [
+ "notification_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "push_deliveries_user_id_users_id_fk": {
+ "name": "push_deliveries_user_id_users_id_fk",
+ "tableFrom": "push_deliveries",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "push_deliveries_subscription_id_push_subscriptions_id_fk": {
+ "name": "push_deliveries_subscription_id_push_subscriptions_id_fk",
+ "tableFrom": "push_deliveries",
+ "tableTo": "push_subscriptions",
+ "columnsFrom": [
+ "subscription_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.push_notifications": {
+ "name": "push_notifications",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "sender_id": {
+ "name": "sender_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "body": {
+ "name": "body",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "push_notifications_created_at_idx": {
+ "name": "push_notifications_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "push_notifications_sender_id_users_id_fk": {
+ "name": "push_notifications_sender_id_users_id_fk",
+ "tableFrom": "push_notifications",
+ "tableTo": "users",
+ "columnsFrom": [
+ "sender_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.push_subscriptions": {
+ "name": "push_subscriptions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "endpoint": {
+ "name": "endpoint",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "p256dh": {
+ "name": "p256dh",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "auth": {
+ "name": "auth",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "device_label": {
+ "name": "device_label",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_success_at": {
+ "name": "last_success_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "push_subscriptions_endpoint_idx": {
+ "name": "push_subscriptions_endpoint_idx",
+ "columns": [
+ {
+ "expression": "endpoint",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "push_subscriptions_user_idx": {
+ "name": "push_subscriptions_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "push_subscriptions_user_id_users_id_fk": {
+ "name": "push_subscriptions_user_id_users_id_fk",
+ "tableFrom": "push_subscriptions",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.task_assignees": {
+ "name": "task_assignees",
+ "schema": "",
+ "columns": {
+ "task_id": {
+ "name": "task_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "task_assignees_user_idx": {
+ "name": "task_assignees_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "task_assignees_task_id_tasks_id_fk": {
+ "name": "task_assignees_task_id_tasks_id_fk",
+ "tableFrom": "task_assignees",
+ "tableTo": "tasks",
+ "columnsFrom": [
+ "task_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "task_assignees_user_id_users_id_fk": {
+ "name": "task_assignees_user_id_users_id_fk",
+ "tableFrom": "task_assignees",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "task_assignees_task_id_user_id_pk": {
+ "name": "task_assignees_task_id_user_id_pk",
+ "columns": [
+ "task_id",
+ "user_id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tasks": {
+ "name": "tasks",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "event_id": {
+ "name": "event_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "lead_time_days": {
+ "name": "lead_time_days",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 7
+ },
+ "lead_time_value": {
+ "name": "lead_time_value",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 7
+ },
+ "lead_time_unit": {
+ "name": "lead_time_unit",
+ "type": "task_lead_time_unit",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'days'"
+ },
+ "due_at": {
+ "name": "due_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "task_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'todo'"
+ },
+ "position": {
+ "name": "position",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "version": {
+ "name": "version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "tasks_event_idx": {
+ "name": "tasks_event_idx",
+ "columns": [
+ {
+ "expression": "event_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "tasks_status_due_idx": {
+ "name": "tasks_status_due_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "due_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "tasks_event_id_events_id_fk": {
+ "name": "tasks_event_id_events_id_fk",
+ "tableFrom": "tasks",
+ "tableTo": "events",
+ "columnsFrom": [
+ "event_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "tasks_lead_time_value_check": {
+ "name": "tasks_lead_time_value_check",
+ "value": "\"tasks\".\"lead_time_value\" >= 0"
+ },
+ "tasks_lead_time_consistency_check": {
+ "name": "tasks_lead_time_consistency_check",
+ "value": "\"tasks\".\"lead_time_days\" = \"tasks\".\"lead_time_value\" * case \"tasks\".\"lead_time_unit\" when 'days' then 1 when 'weeks' then 7 when 'months' then 30 end"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.users": {
+ "name": "users",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "password_hash": {
+ "name": "password_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "role",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'member'"
+ },
+ "telegram_chat_id": {
+ "name": "telegram_chat_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "session_epoch": {
+ "name": "session_epoch",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "onboarding_seen_at": {
+ "name": "onboarding_seen_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "push_enabled": {
+ "name": "push_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "users_email_unique": {
+ "name": "users_email_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "email"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.volunteer_signups": {
+ "name": "volunteer_signups",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "slot_id": {
+ "name": "slot_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "phone": {
+ "name": "phone",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cancel_token": {
+ "name": "cancel_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reminded_at": {
+ "name": "reminded_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "volunteer_signups_slot_idx": {
+ "name": "volunteer_signups_slot_idx",
+ "columns": [
+ {
+ "expression": "slot_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "volunteer_signups_user_idx": {
+ "name": "volunteer_signups_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "volunteer_signups_cancel_token_idx": {
+ "name": "volunteer_signups_cancel_token_idx",
+ "columns": [
+ {
+ "expression": "cancel_token",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "volunteer_signups_slot_email_idx": {
+ "name": "volunteer_signups_slot_email_idx",
+ "columns": [
+ {
+ "expression": "slot_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "lower(\"email\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"volunteer_signups\".\"email\" is not null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "volunteer_signups_slot_id_volunteer_slots_id_fk": {
+ "name": "volunteer_signups_slot_id_volunteer_slots_id_fk",
+ "tableFrom": "volunteer_signups",
+ "tableTo": "volunteer_slots",
+ "columnsFrom": [
+ "slot_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "volunteer_signups_user_id_users_id_fk": {
+ "name": "volunteer_signups_user_id_users_id_fk",
+ "tableFrom": "volunteer_signups",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.volunteer_slots": {
+ "name": "volunteer_slots",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "event_id": {
+ "name": "event_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "start_at": {
+ "name": "start_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "end_at": {
+ "name": "end_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "capacity": {
+ "name": "capacity",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "volunteer_slots_event_idx": {
+ "name": "volunteer_slots_event_idx",
+ "columns": [
+ {
+ "expression": "event_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "volunteer_slots_event_id_events_id_fk": {
+ "name": "volunteer_slots_event_id_events_id_fk",
+ "tableFrom": "volunteer_slots",
+ "tableTo": "events",
+ "columnsFrom": [
+ "event_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {
+ "public.accounting_category_type": {
+ "name": "accounting_category_type",
+ "schema": "public",
+ "values": [
+ "income",
+ "expense"
+ ]
+ },
+ "public.accounting_entry_status": {
+ "name": "accounting_entry_status",
+ "schema": "public",
+ "values": [
+ "draft",
+ "posted"
+ ]
+ },
+ "public.accounting_entry_type": {
+ "name": "accounting_entry_type",
+ "schema": "public",
+ "values": [
+ "income",
+ "expense"
+ ]
+ },
+ "public.association_document_status": {
+ "name": "association_document_status",
+ "schema": "public",
+ "values": [
+ "draft",
+ "final",
+ "archived"
+ ]
+ },
+ "public.association_document_type": {
+ "name": "association_document_type",
+ "schema": "public",
+ "values": [
+ "ag_minutes",
+ "attestation",
+ "statutes",
+ "internal_rules",
+ "insurance",
+ "agreement",
+ "other"
+ ]
+ },
+ "public.association_member_status": {
+ "name": "association_member_status",
+ "schema": "public",
+ "values": [
+ "active",
+ "pending",
+ "inactive"
+ ]
+ },
+ "public.event_status": {
+ "name": "event_status",
+ "schema": "public",
+ "values": [
+ "draft",
+ "published",
+ "archived"
+ ]
+ },
+ "public.financial_account_type": {
+ "name": "financial_account_type",
+ "schema": "public",
+ "values": [
+ "bank",
+ "cash"
+ ]
+ },
+ "public.oauth_token_type": {
+ "name": "oauth_token_type",
+ "schema": "public",
+ "values": [
+ "access",
+ "refresh"
+ ]
+ },
+ "public.outbound_mail_provider": {
+ "name": "outbound_mail_provider",
+ "schema": "public",
+ "values": [
+ "resend",
+ "smtp"
+ ]
+ },
+ "public.outbound_mail_test_status": {
+ "name": "outbound_mail_test_status",
+ "schema": "public",
+ "values": [
+ "untested",
+ "success",
+ "failed"
+ ]
+ },
+ "public.push_delivery_status": {
+ "name": "push_delivery_status",
+ "schema": "public",
+ "values": [
+ "queued",
+ "sent",
+ "failed",
+ "received",
+ "opened"
+ ]
+ },
+ "public.role": {
+ "name": "role",
+ "schema": "public",
+ "values": [
+ "admin",
+ "manager",
+ "member"
+ ]
+ },
+ "public.task_lead_time_unit": {
+ "name": "task_lead_time_unit",
+ "schema": "public",
+ "values": [
+ "days",
+ "weeks",
+ "months"
+ ]
+ },
+ "public.task_status": {
+ "name": "task_status",
+ "schema": "public",
+ "values": [
+ "todo",
+ "in_progress",
+ "done"
+ ]
+ }
+ },
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
\ No newline at end of file
diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json
index ee3d7de..f876cc7 100644
--- a/drizzle/meta/_journal.json
+++ b/drizzle/meta/_journal.json
@@ -120,6 +120,20 @@
"when": 1787454647327,
"tag": "0016_push_notifications",
"breakpoints": true
+ },
+ {
+ "idx": 17,
+ "version": "7",
+ "when": 1788055218908,
+ "tag": "0017_event_ticketing_url",
+ "breakpoints": true
+ },
+ {
+ "idx": 18,
+ "version": "7",
+ "when": 1788055874581,
+ "tag": "0018_whatsapp_group_link",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/src/app/api/events/[id]/route.ts b/src/app/api/events/[id]/route.ts
index 62316e6..a16767a 100644
--- a/src/app/api/events/[id]/route.ts
+++ b/src/app/api/events/[id]/route.ts
@@ -25,6 +25,9 @@ export async function PATCH(req: Request, { params }: Params) {
updates.description = emptyToNull(data.description);
if (data.publicDescription !== undefined)
updates.publicDescription = emptyToNull(data.publicDescription);
+ // Déjà normalisée par le schéma : "" est devenu null, l'URL est absolue.
+ if (data.ticketingUrl !== undefined)
+ updates.ticketingUrl = data.ticketingUrl;
if (data.location !== undefined)
updates.location = emptyToNull(data.location);
if (data.startAt !== undefined) updates.startAt = data.startAt;
@@ -47,6 +50,8 @@ export async function PATCH(req: Request, { params }: Params) {
setFragments.push(
sql`public_description = ${emptyToNull(data.publicDescription)}`,
);
+ if (data.ticketingUrl !== undefined)
+ setFragments.push(sql`ticketing_url = ${data.ticketingUrl}`);
if (data.location !== undefined)
setFragments.push(sql`location = ${emptyToNull(data.location)}`);
if (data.startAt !== undefined)
diff --git a/src/app/api/events/route.ts b/src/app/api/events/route.ts
index 454a923..25527a2 100644
--- a/src/app/api/events/route.ts
+++ b/src/app/api/events/route.ts
@@ -18,6 +18,7 @@ export async function POST(req: Request) {
title: data.title,
description: emptyToNull(data.description),
publicDescription: emptyToNull(data.publicDescription),
+ ticketingUrl: data.ticketingUrl ?? null,
location: emptyToNull(data.location),
startAt: data.startAt,
endAt: data.endAt ?? null,
diff --git a/src/app/dashboard/events/[id]/page.tsx b/src/app/dashboard/events/[id]/page.tsx
index a6174af..c67d78d 100644
--- a/src/app/dashboard/events/[id]/page.tsx
+++ b/src/app/dashboard/events/[id]/page.tsx
@@ -6,6 +6,7 @@ import {
ClipboardCheck,
Download,
CopyPlus,
+ ExternalLink,
Globe,
Landmark,
LayoutDashboard,
@@ -17,6 +18,7 @@ import {
MapPin,
Paperclip,
Pencil,
+ Ticket,
UserRoundPlus,
UsersRound,
WandSparkles,
@@ -346,6 +348,36 @@ export default async function EventDetailPage({
titre, la date et le lieu.
)}
+
+ {/* Lecture seule, et volontairement pas de bouton « copier » : le
+ lien qui doit circuler est celui de la page publique, qui porte
+ les deux démarches. */}
+
+
+
+ Billetterie en ligne
+
+ {event.ticketingUrl ? (
+
+ {event.ticketingUrl}
+
+ Vérifier
+
+ (ouvre un nouvel onglet)
+
+
+ ) : (
+
+ Aucune : la page publique ne propose que les créneaux de
+ bénévoles.
+
+ )}
+
{canManage && (
diff --git a/src/app/dashboard/settings/page.tsx b/src/app/dashboard/settings/page.tsx
index 6775527..8898329 100644
--- a/src/app/dashboard/settings/page.tsx
+++ b/src/app/dashboard/settings/page.tsx
@@ -44,6 +44,7 @@ export default async function SettingsPage() {
recaptchaSecretConfigured: associationStatus.recaptchaSecretConfigured,
recaptchaMinScore: associationStatus.recaptchaMinScore,
recaptchaReady: associationStatus.recaptchaReady,
+ whatsappGroupUrl: associationStatus.whatsappGroupUrl,
legacyEnvironment: associationStatus.legacyEnvironment,
};
const settings: MailSettingsView = {
diff --git a/src/app/inscription/[token]/page.tsx b/src/app/inscription/[token]/page.tsx
index 8316433..4b9801a 100644
--- a/src/app/inscription/[token]/page.tsx
+++ b/src/app/inscription/[token]/page.tsx
@@ -3,9 +3,10 @@ import {
ArrowLeft,
CalendarDays,
CalendarX2,
- CheckCircle2,
+ ExternalLink,
Hand,
MapPin,
+ Ticket,
} from "lucide-react";
import Link from "next/link";
@@ -20,6 +21,7 @@ import {
import { getCurrentUser } from "@/lib/auth/session";
import { getEventByShareToken } from "@/lib/data";
import { formatDateTime } from "@/lib/dates";
+import { ticketingHostLabel } from "@/lib/ticketing";
export const dynamic = "force-dynamic";
@@ -31,9 +33,15 @@ export async function generateMetadata({
const { token } = await params;
const event = await getEventByShareToken(token);
if (!event) return { title: "Inscription bénévole" };
+ // Ce texte est l'aperçu affiché quand le lien circule dans un groupe de
+ // classe : n'annoncer que le bénévolat ferait manquer la page aux familles
+ // qui voulaient réserver.
+ const action = event.ticketingUrl
+ ? "réservez votre place ou donnez un coup de main."
+ : "proposez-vous comme bénévole.";
const desc = `${formatDateTime(event.startAt)}${
event.location ? ` · ${event.location}` : ""
- } — proposez-vous comme bénévole.`;
+ } — ${action}`;
return {
title: event.title,
description: desc,
@@ -87,7 +95,9 @@ export default async function InscriptionPage({
const time = slot.startAt ? ` — ${formatDateTime(slot.startAt)}` : "";
return {
id: slot.id,
- label: `${slot.title}${time} (${remaining} place${remaining > 1 ? "s" : ""})`,
+ // « bénévole recherché », jamais « place » : lu au moment de choisir, le
+ // mot ferait prendre un créneau de buvette pour une entrée réservée.
+ label: `${slot.title}${time} · ${remaining} bénévole${remaining > 1 ? "s" : ""} recherché${remaining > 1 ? "s" : ""}`,
remaining,
};
});
@@ -96,6 +106,11 @@ export default async function InscriptionPage({
(sum, slot) => sum + slot.remaining,
0,
);
+ // `trim()` : un espace collé depuis un traitement de texte est truthy, et
+ // afficherait un bouton « Réserver » qui ne mène nulle part.
+ const billetterie = event.ticketingUrl?.trim() || null;
+ const hote = ticketingHostLabel(billetterie) ?? "la billetterie en ligne";
+ const aDesCreneaux = event.volunteerSlots.length > 0;
return (
@@ -110,20 +125,33 @@ export default async function InscriptionPage({
-
+
-
+ {/* Le padding est porté ici, et non par l'aside : c'est ce qui
+ permet au bandeau de billetterie de le déborder en -mx-6. */}
+
-
- Appel aux bénévoles
+ {billetterie ? (
+ <>
+
+ {aDesCreneaux ? "Billetterie et bénévoles" : "Billetterie en ligne"}
+ >
+ ) : (
+ <>
+
+ Appel aux bénévoles
+ >
+ )}
{event.title}
@@ -160,38 +188,122 @@ export default async function InscriptionPage({
)}
+ {/* Venir et aider sont deux démarches distinctes : elles sont
+ séparées par le contenant — bande turquoise traversante d'un
+ côté, carte blanche du formulaire de l'autre — et non par la
+ couleur d'un bouton, qui ne survivrait ni au daltonisme ni au
+ niveau de gris. La bande est ici, avant la description : sa
+ position ne dépend donc pas de la longueur du texte saisi. */}
+ {billetterie && (
+
+
+ Venir à l’événement
+
+
+ Je réserve ma place
+
+
+ La réservation se fait sur {hote}, la plateforme utilisée par
+ l’association. Vous y choisissez vos places et réglez en
+ ligne s’il y a un tarif.
+
+
+
+ Réserver sur {hote}
+
+ (ouvre un nouvel onglet)
+
+
+ Vous quittez le site de l’association. {hote} vous enverra
+ votre confirmation : elle n’apparaîtra pas sur cette page.
+
+
+ Réserver ne vous inscrit pas comme bénévole : ce sont deux
+ démarches indépendantes.
+
+
+ )}
+
{event.publicDescription && (
)}
-
-
-
- {availablePlaces} place{availablePlaces > 1 ? "s" : ""} encore
- disponible{availablePlaces > 1 ? "s" : ""}
-
-
+ {/* Sans ce garde, un événement à billetterie seule afficherait
+ « 0 coup de main » à côté du bouton « Réserver » : le parent
+ lit « complet » et referme. */}
+ {aDesCreneaux && (
+
+
+
+ {availablePlaces > 0
+ ? `${availablePlaces} coup${availablePlaces > 1 ? "s" : ""} de main encore recherché${availablePlaces > 1 ? "s" : ""}`
+ : "Toutes les missions sont pourvues, merci !"}
+
+
+ )}
- Ma participation
+ Aider à l’organisation
Je donne un coup de main
- Choisissez une mission et laissez vos coordonnées. Cela prend
- moins d'une minute.
+ {billetterie ? (
+ <>
+ Choisissez une mission et laissez vos coordonnées. Gratuit,
+ sans compte, en moins d’une minute. Cela ne réserve pas votre
+ place à l’événement — pour venir, c’est « Je réserve ma
+ place ».
+ >
+ ) : (
+ <>
+ Choisissez une mission et laissez vos coordonnées. Cela prend
+ moins d’une minute.
+ >
+ )}
{event.volunteerSlots.length === 0 ? (
- Aucun créneau de bénévolat n'est ouvert pour le moment.
- Revenez bientôt, les besoins seront précisés ici.
+ {billetterie ? (
+ <>
+ Aucun créneau de bénévolat n’est ouvert pour l’instant.
+ Votre réservation, elle, est déjà possible : c’est « Je
+ réserve ma place ».
+ >
+ ) : (
+ <>
+ Aucun créneau de bénévolat n’est ouvert pour le moment.
+ Revenez bientôt, les besoins seront précisés ici.
+ >
+ )}
) : (
)}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 1f548fe..09310d0 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -5,7 +5,9 @@ import {
Hand,
HeartHandshake,
MapPin,
+ MessageCircle,
MousePointerClick,
+ Ticket,
} from "lucide-react";
import Link from "next/link";
@@ -242,12 +244,25 @@ export default async function HomePage() {
{event.title}
- {remaining > 0 && (
-
-
- {remaining} place{remaining > 1 ? "s" : ""}
-
- )}
+
+ {/* Non cliquable : la carte entière est déjà un
+ lien, une ancre imbriquée serait invalide — et
+ personne ne doit partir payer depuis l'accueil
+ sans avoir lu la date ni le lieu. */}
+ {event.ticketingUrl && (
+
+
+ Billetterie
+
+ )}
+ {remaining > 0 && (
+
+
+ {remaining} coup{remaining > 1 ? "s" : ""} de
+ main
+
+ )}
+
@@ -266,7 +281,7 @@ export default async function HomePage() {
/>
)}
- {remaining > 0
+ {remaining > 0 && !event.ticketingUrl
? "Voir et se proposer"
: "Voir l’événement"}
@@ -333,6 +348,20 @@ export default async function HomePage() {
),
)}
+
+ {settings.whatsappGroupUrl && (
+
+
+ Toutes les infos au fil de l’eau : le groupe WhatsApp de
+ l’association
+ (ouvre WhatsApp dans un nouvel onglet)
+
+ )}
diff --git a/src/app/rejoindre/page.tsx b/src/app/rejoindre/page.tsx
index 9c5237c..218ea38 100644
--- a/src/app/rejoindre/page.tsx
+++ b/src/app/rejoindre/page.tsx
@@ -1,27 +1,47 @@
import {
ArrowLeft,
+ ArrowRight,
CalendarDays,
Coins,
+ Hand,
HeartHandshake,
Mail,
+ MapPin,
MessagesSquare,
- Sparkles,
Users,
} from "lucide-react";
import type { Metadata } from "next";
+import Image from "next/image";
import Link from "next/link";
import { JoinForm } from "@/components/join-form";
import { SiteFooter } from "@/components/site-footer";
import { SiteHeader } from "@/components/site-header";
+import { WhatsappInviteCard } from "@/components/whatsapp-invite-card";
+import { getUpcomingPublishedEvents } from "@/lib/data";
+import { formatDateTime, formatLongDateTime } from "@/lib/dates";
import { getAssociationSettings } from "@/lib/services/association-settings";
export const dynamic = "force-dynamic";
+/**
+ * Page publique « Rejoindre l'association ».
+ *
+ * Elle s'adresse à un parent qui ne connaît personne : elle répond d'abord à ce
+ * qui le retient, montre ensuite que l'association existe pour de vrai — les
+ * rendez-vous réellement publiés, les places qui restent — puis lui laisse deux
+ * portes, l'une basse (prendre un créneau sans compte), l'autre écrite.
+ *
+ * Règle de confidentialité : getUpcomingPublishedEvents() rapporte aussi les
+ * inscriptions des bénévoles (nom, e-mail, jeton d'annulation). Tout reste dans
+ * ce composant serveur ; ne transmettre à un composant client que des primitives
+ * déjà calculées, jamais un événement ou un créneau entier.
+ */
+
export async function generateMetadata(): Promise {
const settings = await getAssociationSettings();
const title = `Rejoindre ${settings.associationName}`;
- const description = `Ce que fait ${settings.associationName} à ${settings.schoolName}, et comment en être.`;
+ const description = `Ce que fait ${settings.associationName} à ${settings.schoolName}, et comment y prendre part — sans engagement.`;
return {
title,
description,
@@ -29,165 +49,680 @@ export async function generateMetadata(): Promise {
};
}
-const ACTIONS = [
+const OBJECTIONS = [
+ {
+ phrase: "Je n’ai pas le temps.",
+ reponse:
+ "Personne n’en a. C’est pour ça que tout est découpé en créneaux courts, annoncés à l’avance. Deux heures un samedi matin, une fois dans l’année, c’est déjà un vrai coup de main — et personne ici ne tient le compte des heures des autres.",
+ },
+ {
+ phrase: "Je ne connais personne.",
+ reponse:
+ "C’est exactement ce que préparer quelque chose ensemble règle en une matinée. On installe à deux, on sert à trois, on range à plusieurs, et on repart en connaissant des visages. Les familles qui viennent d’arriver entrent souvent par là.",
+ },
+ {
+ phrase: "Je ne saurais pas quoi faire.",
+ reponse:
+ "Il n’y a rien à savoir d’avance. Chaque créneau porte un intitulé, un horaire et le nombre de personnes attendues : vous choisissez celui qui vous va, et on vous montre en arrivant.",
+ },
+ {
+ phrase: "Et si je dis oui, puis que je ne peux plus ?",
+ reponse:
+ "Ça arrive, et c’est prévu. L’e-mail de confirmation contient un lien qui retire votre inscription en un clic, sans avoir à vous justifier ni à prévenir qui que ce soit.",
+ },
+];
+
+const MISSIONS = [
{
icon: CalendarDays,
- title: "Organiser les temps forts",
- text: "Kermesse, vide-grenier, boom, venue du Père Noël : l’association monte les rendez-vous qui font l’année des enfants.",
+ numero: "01",
+ titre: "Les temps forts de l’année",
+ texte:
+ "L’association monte les rendez-vous qui font l’année des enfants : ceux qu’on prépare des semaines à l’avance et dont ils parlent encore en juin.",
},
{
icon: Coins,
- title: "Financer les projets de l’école",
- text: "Les bénéfices des ventes et des événements reviennent aux classes : sorties, spectacles, matériel, voyages.",
+ numero: "02",
+ titre: "Les projets des classes",
+ texte:
+ "Ce que rapportent les ventes et les événements repart vers les classes : sorties, spectacles, matériel.",
},
{
icon: MessagesSquare,
- title: "Porter la voix des parents",
- text: "L’association représente les familles auprès de la direction et relaie ce qui compte pour elles.",
+ numero: "03",
+ titre: "La voix des parents",
+ texte:
+ "L’association représente les familles auprès de la direction et fait remonter ce qui compte pour elles. Une voix collective porte plus loin qu’un mot dans un carnet.",
},
{
icon: Users,
- title: "Créer du lien entre familles",
- text: "On se croise, on prépare, on partage un café : c’est souvent là que les nouvelles familles trouvent leur place.",
+ numero: "04",
+ titre: "Le lien entre les familles",
+ texte:
+ "On prépare ensemble, on discute en rangeant, et l’école devient un endroit où l’on connaît des visages.",
},
];
-const ENGAGEMENTS = [
- "Deux heures sur un stand, une fois dans l’année : c’est déjà un vrai coup de main.",
- "Un savoir-faire ponctuel — affiches, pâtisserie, bricolage, photos — sans réunion à la clé.",
- "Une place au bureau, pour celles et ceux qui veulent s’impliquer davantage.",
+const ETAPES = [
+ {
+ titre: "Vous écrivez",
+ texte: "Deux ou trois lignes suffisent : qui vous êtes, ce qui vous intrigue.",
+ },
+ {
+ titre: "On vous répond",
+ texte: "Un parent de l’équipe vous répond par e-mail.",
+ },
+ {
+ titre: "Vous décidez",
+ texte:
+ "Rien n’est engagé tant que vous n’avez pas dit oui. Ce message n’est ni une adhésion ni une inscription, et il n’y a rien à payer sur cette page.",
+ },
+];
+
+const AMORCES = [
+ "Je peux venir donner un coup de main sur un événement, prévenez-moi.",
+ "Je ne sais pas encore, j’aimerais surtout savoir ce qui se prépare.",
+ "J’aimerais m’impliquer davantage, comment ça marche ?",
+];
+
+/** Teintes des tuiles de mission, pour éviter quatre boîtes blanches identiques. */
+const TEINTES_MISSIONS = [
+ {
+ fond: "bg-brand-950 text-white",
+ pastille: "bg-white text-brand-950",
+ numero: "text-brand-300",
+ texte: "text-brand-100",
+ },
+ {
+ fond: "bg-brand-100 text-brand-950",
+ pastille: "bg-brand-950 text-white",
+ numero: "text-brand-800",
+ texte: "text-brand-900",
+ },
+ {
+ fond: "bg-sea-200 text-brand-950",
+ pastille: "bg-brand-950 text-white",
+ numero: "text-brand-700",
+ texte: "text-brand-950",
+ },
+ {
+ fond: "bg-brand-950 text-white",
+ pastille: "bg-white text-brand-950",
+ numero: "text-brand-300",
+ texte: "text-brand-100",
+ },
];
export default async function RejoindrePage() {
- const settings = await getAssociationSettings();
+ const [settings, events] = await Promise.all([
+ getAssociationSettings(),
+ getUpcomingPublishedEvents(),
+ ]);
+
+ // Agrégats calculés ici, côté serveur : rien de ce qui vient de la base ne
+ // descend jusqu'au navigateur en dehors de ces nombres et de ces chaînes.
+ const placesOuvertes = events.reduce(
+ (total, event) =>
+ total +
+ event.volunteerSlots.reduce(
+ (somme, slot) => somme + Math.max(0, slot.capacity - slot.signups.length),
+ 0,
+ ),
+ 0,
+ );
+ const prochains = events.slice(0, 3).map((event, index) => ({
+ id: event.id,
+ titre: event.title,
+ date: formatDateTime(event.startAt),
+ lieu: event.location,
+ jeton: event.shareToken,
+ restantes: event.volunteerSlots.reduce(
+ (somme, slot) => somme + Math.max(0, slot.capacity - slot.signups.length),
+ 0,
+ ),
+ filet: index % 2 === 0 ? "bg-brand-700" : "bg-sea-500",
+ }));
+ const prochain = events[0]
+ ? {
+ titre: events[0].title,
+ date: formatLongDateTime(events[0].startAt),
+ jeton: events[0].shareToken,
+ }
+ : null;
+ const contactEmail = settings.contactEmail?.trim() || null;
+ const rna = settings.rna?.trim() || null;
return (
+ {/* ————— Héros : la promesse, deux portes d'action, et la preuve chiffrée ————— */}
-
-
-
- Retour à l’accueil
-
-
-
- {settings.associationName}
-
-
- Rejoindre l’association
-
-
- {settings.associationName} réunit les parents d’élèves de{" "}
- {settings.schoolName}. On y donne le temps qu’on a, pas celui
- qu’on n’a pas — et chaque coup de main sert directement aux
- enfants.
-
+
+
+
+
+
+
+
+ {settings.associationName} · {settings.schoolName}
+
+
+ On donne le temps qu’on a,{" "}
+ pas celui qu’on n’a pas.
+
+
+ {settings.associationName} réunit les parents d’élèves de{" "}
+ {settings.schoolName}. On y prépare les rendez-vous de l’année
+ et on cherche des mains pour les tenir. Deux heures une fois, ou
+ davantage si l’envie vient : rien n’est obligatoire, et rien
+ n’est décidé d’avance.
+
+
+
+
+
+
+
+
+
+
+ Association de parents d’élèves
+
+
+ {settings.schoolName}
+
+
+
+
+
+ {prochain ? (
+
+
+
+
+
+
+ Prochain rendez-vous
+
+
+ {prochain.titre}
+
+
+ {prochain.date}
+
+
+
+
+ ) : (
+
+
+ Rien de publié en ce moment
+
+
+ L’agenda se remplit d’une période à l’autre. Écrivez-nous :
+ on vous préviendra du prochain rendez-vous plutôt que de
+ vous laisser guetter la page.
+
+
+ )}
+
+ {events.length > 0 && (
+
+
+
+ {events.length}
+
+
+ rendez-vous à venir
+
+
+ {placesOuvertes > 0 ? (
+
+
+ {placesOuvertes}
+
+
+ coup{placesOuvertes > 1 ? "s" : ""} de main recherché
+ {placesOuvertes > 1 ? "s" : ""}
+
+
+ ) : (
+
+ Toutes les places sont prises pour l’instant.
+
+ )}
+
+ )}
+
+
-
-
-
- Ce que fait l’association
+ {/* ————— Respiration : une phrase, un aplat, aucune demande ————— */}
+
+
+
+
+ Une école qui bouge, ce sont des parents qui s’y mettent.
-
- Quatre missions, toute l’année
-
-
- {ACTIONS.map(({ icon: Icon, title, text }) => (
-
+
+
+ {/* ————— Le mur des « oui, mais… » : le seul bloc qui tient debout sans données ————— */}
+
+
+
+
+ Les quatre phrases qu’on entend le plus
+
+
+ Ce qui retient, et ce qu’on répond.
+
+
+
+
+ {OBJECTIONS.map(({ phrase, reponse }, index) => (
+
-
-
-
-
- {title}
-
-
- {text}
+
+ « {phrase} »
-
+
+
))}
+
+
+
+
+ Aucune de ces phrases n’est un obstacle.
+
+
-
-
-
-
-
-
+ {/* ————— La porte basse : les rendez-vous réels, où l'on prend un créneau sans compte ————— */}
+
+
+
+
+
+ Sans écrire à personne
+
+
+ Vous pouvez aussi juste venir voir.
+
+
+
+ Ce ne sont pas des exemples : ce sont les rendez-vous déjà
+ publiés par l’association, avec les places qui restent à prendre.
+
+
+
+ {prochains.length === 0 ? (
+
+
+
-
-
- S’engager, à la mesure de chacun
-
-
+ ) : (
+ <>
+
+ {prochains.map((event) => (
+
+
-
+
+
+
+
+ {event.date}
+
+
+ {event.titre}
+
+ {event.lieu && (
+
+
+ {event.lieu}
+
+ )}
+
+ {event.restantes > 0 && (
+
+
+ {event.restantes} place
+ {event.restantes > 1 ? "s" : ""}
+
+ )}
+
+ {event.restantes > 0
+ ? "Se proposer"
+ : "Voir le rendez-vous"}
+
+
+
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+ On prend un créneau directement sur la page de l’événement :
+ sans créer de compte, sans être adhérent, et sans passer par
+ nous.
+
+ {events.length > 3 && (
+
+ Voir tous les rendez-vous
+
+
+ )}
+
+ >
+ )}
+
+
+
+ {/* ————— Les missions : la théorie, compacte et rétrogradée ————— */}
+
+
+
+
+ Où va votre coup de main
+
+
+ Tout ce que fait l’association revient à l’école.
+
+
+
+
+ {MISSIONS.map(({ icon: Icon, numero, titre, texte }, index) => {
+ const teinte = TEINTES_MISSIONS[index];
+ return (
+
+
+
+
- {item}
-
- ))}
-
-
-
+
+
+ {numero}
+
+
+
+ {titre}
+
+
+ {texte}
+
+
+ );
+ })}
-
-
-
- Prendre contact
-
-
- Écrivez-nous
-
-
- Pas besoin de savoir déjà ce que vous voulez faire : dites-nous
- qui vous êtes, on vous rappellera ce qui se prépare.
- {settings.contactEmail && (
- <>
- {" "}
- Vous préférez votre messagerie ?{" "}
-
-
- {settings.contactEmail}
-
- >
- )}
-
-
-
+
+
+
+ )}
+
+ {/* ————— Le coupon-réponse : ce qui se passe après l'envoi, puis le formulaire ————— */}
+
diff --git a/src/components/association-settings-form.tsx b/src/components/association-settings-form.tsx
index 4a33b8c..b383bad 100644
--- a/src/components/association-settings-form.tsx
+++ b/src/components/association-settings-form.tsx
@@ -6,6 +6,7 @@ import {
Building2,
CircleAlert,
KeyRound,
+ MessageCircle,
Save,
ShieldCheck,
} from "lucide-react";
@@ -16,6 +17,7 @@ import { LogoUploadField } from "@/components/logo-upload-field";
import { useToast } from "@/components/toast";
import { Button, Card, Field, Input, Select } from "@/components/ui";
import { api } from "@/lib/client";
+import { checkWhatsappUrl, estInvitationGroupe } from "@/lib/whatsapp";
export interface AssociationSettingsView {
associationName: string;
@@ -36,6 +38,7 @@ export interface AssociationSettingsView {
recaptchaSecretConfigured: boolean;
recaptchaMinScore: number;
recaptchaReady: boolean;
+ whatsappGroupUrl: string | null;
legacyEnvironment: boolean;
}
@@ -47,6 +50,17 @@ export function AssociationSettingsForm({
const router = useRouter();
const toast = useToast();
const [saving, setSaving] = useState(false);
+ const [whatsappGroupUrl, setWhatsappGroupUrl] = useState(
+ settings.whatsappGroupUrl ?? "",
+ );
+ const [erreurWhatsapp, setErreurWhatsapp] = useState(null);
+ const verdictWhatsapp = checkWhatsappUrl(whatsappGroupUrl);
+ // Prévient sans bloquer : un lien raccourci ou une redirection maison reste
+ // acceptable, mais l'administrateur doit voir qu'il n'a pas collé l'invitation.
+ const whatsappInhabituel =
+ verdictWhatsapp.ok &&
+ verdictWhatsapp.url !== null &&
+ !estInvitationGroupe(verdictWhatsapp.url);
async function save(event: React.FormEvent) {
event.preventDefault();
@@ -57,6 +71,16 @@ export function AssociationSettingsForm({
).trim();
const recaptchaSecret = String(form.get("recaptchaSecret") ?? "").trim();
+ // Contrôlé ici en plus du serveur : handleApiError réduit toute erreur zod
+ // à « Données invalides », le message utile ne serait jamais lu.
+ const whatsapp = checkWhatsappUrl(whatsappGroupUrl);
+ if (!whatsapp.ok) {
+ setErreurWhatsapp(whatsapp.message);
+ setSaving(false);
+ document.getElementById("whatsapp-group-url")?.focus();
+ return;
+ }
+
try {
await api("/api/settings/association", {
method: "PATCH",
@@ -80,6 +104,7 @@ export function AssociationSettingsForm({
recaptchaEnabled: form.get("recaptchaEnabled") === "on",
recaptchaSiteKey: form.get("recaptchaSiteKey") || null,
recaptchaMinScore: Number(form.get("recaptchaMinScore")),
+ whatsappGroupUrl: whatsapp.url,
clearRecaptchaSecret:
!recaptchaSecret && form.get("clearRecaptchaSecret") === "on",
...(recaptchaSecret ? { recaptchaSecret } : {}),
@@ -337,6 +362,60 @@ export function AssociationSettingsForm({
+
+
+
+
+
+
+
Groupe WhatsApp
+
+ Annoncé aux familles sur la page « Rejoindre l’association »
+ et dans le pied de page du site public.
+
+
+
+
+ {
+ setWhatsappGroupUrl(event.target.value);
+ setErreurWhatsapp(null);
+ }}
+ onBlur={() => {
+ const verdict = checkWhatsappUrl(whatsappGroupUrl);
+ setErreurWhatsapp(verdict.ok ? null : verdict.message);
+ }}
+ aria-invalid={erreurWhatsapp ? true : undefined}
+ aria-describedby="whatsapp-group-url-etat"
+ placeholder="https://chat.whatsapp.com/…"
+ />
+
+ {erreurWhatsapp ??
+ (whatsappInhabituel
+ ? "Ce lien ne ressemble pas à une invitation de groupe WhatsApp — il sera tout de même enregistré et affiché."
+ : "Facultatif. Sans lien, rien n’est annoncé sur le site public.")}
+
+
+
+
diff --git a/src/components/event-form.tsx b/src/components/event-form.tsx
index 9c75329..6e3f319 100644
--- a/src/components/event-form.tsx
+++ b/src/components/event-form.tsx
@@ -1,6 +1,6 @@
"use client";
-import { Globe, Lock } from "lucide-react";
+import { Globe, Lock, Ticket } from "lucide-react";
import { useRouter } from "next/navigation";
import { useState, type ReactNode } from "react";
@@ -9,6 +9,7 @@ import { Button, Input, Label, Select } from "@/components/ui";
import { ApiError, api } from "@/lib/client";
import { toDatetimeLocal } from "@/lib/dates";
import type { Event } from "@/lib/db/schema";
+import { checkTicketingUrl, ticketingHostLabel } from "@/lib/ticketing";
import { useMutationError } from "@/lib/use-mutation-error";
export function EventForm({
@@ -28,6 +29,15 @@ export function EventForm({
const [publicDescription, setPublicDescription] = useState(
event?.publicDescription ?? "",
);
+ // `?? ""` obligatoire : la colonne vaut null en base, et un input contrôlé
+ // qui reçoit null repasse en non contrôlé.
+ const [ticketingUrl, setTicketingUrl] = useState(event?.ticketingUrl ?? "");
+ const [erreurBilletterie, setErreurBilletterie] = useState(null);
+ const verdictBilletterie = checkTicketingUrl(ticketingUrl);
+ // Sert à prévenir sans bloquer quand la billetterie n'est pas HelloAsso.
+ const hoteBilletterie = verdictBilletterie.ok
+ ? ticketingHostLabel(verdictBilletterie.url)
+ : null;
async function onSubmit(e: React.FormEvent) {
e.preventDefault();
@@ -35,10 +45,21 @@ export function EventForm({
setLoading(true);
const form = new FormData(e.currentTarget);
const endRaw = form.get("endAt");
+
+ // Contrôlée ici en plus du serveur : handleApiError réduit toute erreur zod
+ // à « Données invalides », le message utile ne serait jamais lu.
+ const billetterie = checkTicketingUrl(ticketingUrl);
+ if (!billetterie.ok) {
+ setErreurBilletterie(billetterie.message);
+ setLoading(false);
+ document.getElementById("ticketingUrl")?.focus();
+ return;
+ }
const body = {
title: form.get("title"),
description,
publicDescription,
+ ticketingUrl: billetterie.url,
location: form.get("location"),
startAt: form.get("startAt"),
endAt: endRaw ? endRaw : null,
@@ -180,6 +201,61 @@ export function EventForm({
+
+
+
+
+ Lien de réservation (HelloAsso ou autre)
+
+
+
+ Ce lien sert aux familles qui veulent venir : il ne
+ remplace pas les créneaux de bénévoles, qui restent gérés ici.
+ Ouvrez votre billetterie dans le navigateur et copiez l’adresse
+ affichée dans la barre du haut.
+
+
refuse
+ // « www.helloasso.com/… » sans schéma avec une bulle du navigateur,
+ // dans la langue du système, avant que nos messages ne s'affichent.
+ type="text"
+ inputMode="url"
+ spellCheck={false}
+ value={ticketingUrl}
+ onChange={(e) => {
+ setTicketingUrl(e.target.value);
+ setErreurBilletterie(null);
+ }}
+ onBlur={() => {
+ const verdict = checkTicketingUrl(ticketingUrl);
+ setErreurBilletterie(verdict.ok ? null : verdict.message);
+ }}
+ aria-invalid={erreurBilletterie ? true : undefined}
+ aria-describedby="ticketingUrl-aide"
+ placeholder="https://www.helloasso.com/associations/…/evenements/…"
+ />
+
+ {erreurBilletterie ??
+ (hoteBilletterie && hoteBilletterie !== "HelloAsso"
+ ? `Ce n’est pas un lien HelloAsso : les familles verront « sur ${hoteBilletterie} ». C’est accepté, vérifiez simplement que la page est bien celle de la réservation.`
+ : "Facultatif. Sans lien, la page publique ne parle que des créneaux de bénévoles.")}
+
+
+
+
+
@@ -201,7 +277,7 @@ export function EventForm({
{!editing && templates.length > 0 && (
diff --git a/src/components/join-form.tsx b/src/components/join-form.tsx
index e2679a1..704946c 100644
--- a/src/components/join-form.tsx
+++ b/src/components/join-form.tsx
@@ -10,9 +10,12 @@ import { useRecaptcha } from "@/lib/use-recaptcha";
export function JoinForm({
contactEmail,
recaptchaSiteKey = null,
+ whatsappGroupUrl = null,
}: {
contactEmail: string | null;
recaptchaSiteKey?: string | null;
+ /** Proposé pendant l'attente de la réponse : on suit sans avoir écrit. */
+ whatsappGroupUrl?: string | null;
}) {
const executerRecaptcha = useRecaptcha(recaptchaSiteKey);
const [loading, setLoading] = useState(false);
@@ -67,6 +70,22 @@ export function JoinForm({
) : null}
.
+ {whatsappGroupUrl && (
+
+ En attendant notre réponse, vous pouvez déjà suivre ce qui se
+ prépare :{" "}
+
+ le groupe WhatsApp de l’association
+ (ouvre WhatsApp dans un nouvel onglet)
+
+ .
+
+ )}
);
}
diff --git a/src/components/site-footer.tsx b/src/components/site-footer.tsx
index 31d8ddf..e758815 100644
--- a/src/components/site-footer.tsx
+++ b/src/components/site-footer.tsx
@@ -1,4 +1,4 @@
-import { Github } from "lucide-react";
+import { Github, MessageCircle } from "lucide-react";
import Link from "next/link";
import { PROJECT_REPOSITORY_URL } from "@/lib/app-config";
@@ -18,6 +18,18 @@ export async function SiteFooter() {
+ {settings.whatsappGroupUrl && (
+
+
+ Groupe WhatsApp
+ (ouvre WhatsApp dans un nouvel onglet)
+
+ )}
s.remaining > 0);
@@ -36,7 +49,7 @@ export function VolunteerSignupForm({
if (available.length === 0) {
return (
- Tous les créneaux sont actuellement complets. Merci de votre intérêt !
+ Tous les créneaux sont pourvus. Merci de votre intérêt !
);
}
@@ -46,17 +59,47 @@ export function VolunteerSignupForm({
);
@@ -173,8 +216,10 @@ export function VolunteerSignupForm({
+ {/* « Je m'inscris » est le mot qu'un parent emploie pour inscrire son
+ enfant à la kermesse : il ne peut pas désigner ici le bénévolat. */}
- Je m’inscris
+ Je prends ce créneau
);
diff --git a/src/components/whatsapp-invite-card.tsx b/src/components/whatsapp-invite-card.tsx
new file mode 100644
index 0000000..b1db628
--- /dev/null
+++ b/src/components/whatsapp-invite-card.tsx
@@ -0,0 +1,150 @@
+import {
+ ArrowUpRight,
+ CalendarDays,
+ HandHeart,
+ Megaphone,
+ MessageCircle,
+} from "lucide-react";
+
+/**
+ * Carton d'invitation au groupe WhatsApp de l'association.
+ *
+ * L'aperçu de conversation est une illustration, et il est cadré comme telle :
+ * la légende « Exemple de ce qui circule dans le groupe » est visible et placée
+ * avant les bulles, de sorte que personne — voyant ou non — ne puisse prendre
+ * ces trois phrases pour des messages réellement envoyés. Pas d'horodatage, pas
+ * de double coche, pas de prénom, pas d'émoticône : une seule voix, celle de
+ * l'association, et rien qui ressemble à une capture d'écran.
+ *
+ * Le vert de la marque n'apparaît qu'aux deux endroits où il signifie WhatsApp
+ * — le sceau et le bouton — et porte une encre bleu nuit : du blanc sur ce vert
+ * tomberait à 1,98:1, la « correction » que tout relecteur propose au premier
+ * coup d'œil. Le reste est la palette du site.
+ */
+
+const APERCU = [
+ {
+ icon: CalendarDays,
+ texte: "Les dates des rendez-vous, avec un rappel avant le jour J.",
+ largeur: "max-w-[26rem]",
+ },
+ {
+ icon: Megaphone,
+ texte: "Les changements de dernière minute, annoncés là où on les lit vite.",
+ largeur: "max-w-[30rem]",
+ },
+ {
+ icon: HandHeart,
+ texte: "Les coups de main cherchés, quand il manque quelqu’un sur un créneau.",
+ largeur: "max-w-[24rem]",
+ },
+];
+
+export function WhatsappInviteCard({
+ url,
+ associationName,
+}: {
+ url: string;
+ associationName: string;
+}) {
+ return (
+
+
+ {/* Double filet : la signature du carton gravé. */}
+
+
+
+
+
+
+
+
+
+
+ Vous êtes invités
+
+
+ Toutes les infos, dans un seul groupe WhatsApp
+
+
+ {associationName} tient un groupe WhatsApp ouvert aux familles.
+ Les dates, les rappels, les changements de dernière minute et les
+ coups de main cherchés y sont annoncés : vous suivez ce qui se
+ prépare sans avoir à le chercher.
+
+
+
+
+
+
+ Exemple de ce qui circule dans le groupe.
+
+
+ {APERCU.map(({ icon: Icon, texte, largeur }) => (
+
+
+
+
+
+
+
+ {texte}
+
+
+
+ ))}
+
+
+
+
+
+
+ );
+}
diff --git a/src/lib/db/schema.ts b/src/lib/db/schema.ts
index 94257cc..da3a483 100644
--- a/src/lib/db/schema.ts
+++ b/src/lib/db/schema.ts
@@ -145,6 +145,13 @@ export const events = pgTable("events", {
description: text("description"),
/** Texte affiché sur l'accueil public et sur la page d'inscription. */
publicDescription: text("public_description"),
+ /**
+ * Billetterie ou réservation en ligne de l'événement (HelloAsso le plus
+ * souvent). Présentée aux familles comme une action distincte du bénévolat :
+ * « je réserve ma place » n'est pas « je donne un coup de main ».
+ * `null` quand l'événement n'a pas de billetterie — rien n'est alors affiché.
+ */
+ ticketingUrl: text("ticketing_url"),
location: text("location"),
startAt: timestamp("start_at", { withTimezone: true }).notNull(),
endAt: timestamp("end_at", { withTimezone: true }),
@@ -603,6 +610,12 @@ export const associationSettings = pgTable(
recaptchaSiteKey: text("recaptcha_site_key"),
encryptedRecaptchaSecret: text("encrypted_recaptcha_secret"),
recaptchaMinScore: integer("recaptcha_min_score").notNull().default(50),
+ /**
+ * Lien d'invitation au groupe WhatsApp, affiché sur les pages publiques.
+ * Vide : aucun encart n'est rendu. Ce n'est pas un secret — il est fait pour
+ * être publié — mais il n'a rien à faire dans un journal d'audit.
+ */
+ whatsappGroupUrl: text("whatsapp_group_url"),
/**
* Paire de clés VAPID des notifications sur appareil, engendrée au premier
* abonnement : rien à créer ni à recopier pour l'exploitant. La clé
diff --git a/src/lib/mcp/register-core-tools.ts b/src/lib/mcp/register-core-tools.ts
index 0f4f77f..e36c3fb 100644
--- a/src/lib/mcp/register-core-tools.ts
+++ b/src/lib/mcp/register-core-tools.ts
@@ -178,6 +178,8 @@ export function registerCoreTools(
description: optionalNullableString,
/** Affiché sur l'accueil public et sur la page d'inscription. */
publicDescription: optionalNullableString,
+ /** Billetterie en ligne (HelloAsso…) : « je réserve ma place ». */
+ ticketingUrl: optionalNullableString,
location: optionalNullableString,
startAt: localDateTime,
endAt: localDateTime.nullable().optional(),
@@ -194,6 +196,7 @@ export function registerCoreTools(
title: data.title,
description: emptyToNull(data.description),
publicDescription: emptyToNull(data.publicDescription),
+ ticketingUrl: data.ticketingUrl ?? null,
location: emptyToNull(data.location),
startAt: data.startAt,
endAt: data.endAt ?? null,
@@ -227,6 +230,8 @@ export function registerCoreTools(
description: optionalNullableString,
/** Affiché sur l'accueil public et sur la page d'inscription. */
publicDescription: optionalNullableString,
+ /** Billetterie en ligne (HelloAsso…) : « je réserve ma place ». */
+ ticketingUrl: optionalNullableString,
location: optionalNullableString,
startAt: localDateTime.optional(),
endAt: localDateTime.nullable().optional(),
@@ -244,6 +249,8 @@ export function registerCoreTools(
updates.description = emptyToNull(data.description);
if (data.publicDescription !== undefined)
updates.publicDescription = emptyToNull(data.publicDescription);
+ if (data.ticketingUrl !== undefined)
+ updates.ticketingUrl = data.ticketingUrl;
if (data.location !== undefined)
updates.location = emptyToNull(data.location);
if (data.startAt !== undefined) updates.startAt = data.startAt;
diff --git a/src/lib/notifications/emails.ts b/src/lib/notifications/emails.ts
index 2ced5df..aed3d93 100644
--- a/src/lib/notifications/emails.ts
+++ b/src/lib/notifications/emails.ts
@@ -60,11 +60,13 @@ export function volunteerConfirmationEmail(ctx: VolunteerCtx): EmailContent {
? `
Lieu : ${esc(ctx.location)} `
: "";
return {
- subject: `Inscription confirmée — ${ctx.eventTitle}`,
+ // « Inscription confirmée », lu seul dans une boîte mail trois jours plus
+ // tard, se prend pour une confirmation de billet.
+ subject: `Votre créneau du ${ctx.eventDate} — ${ctx.eventTitle}`,
html: layout(
- "Merci pour votre inscription ! 🎉",
+ "Merci pour votre coup de main ! 🎉",
`
Bonjour ${esc(ctx.name)},
-
Votre inscription comme bénévole est bien enregistrée :
+
Votre créneau de bénévole est bien enregistré :
Événement : ${esc(ctx.eventTitle)}
Date : ${ctx.eventDate}
@@ -75,7 +77,7 @@ export function volunteerConfirmationEmail(ctx: VolunteerCtx): EmailContent {
${button(ctx.cancelUrl, "Me désinscrire")}
`,
ctx.identity,
),
- text: `Bonjour ${ctx.name},\n\nInscription confirmée :\n- Événement : ${ctx.eventTitle}\n- Date : ${ctx.eventDate}\n- Créneau : ${ctx.slotTitle}\n${ctx.location ? `- Lieu : ${ctx.location}\n` : ""}\nMe désinscrire : ${ctx.cancelUrl}`,
+ text: `Bonjour ${ctx.name},\n\nCréneau de bénévole confirmé :\n- Événement : ${ctx.eventTitle}\n- Date : ${ctx.eventDate}\n- Créneau : ${ctx.slotTitle}\n${ctx.location ? `- Lieu : ${ctx.location}\n` : ""}\nMe désinscrire : ${ctx.cancelUrl}`,
};
}
diff --git a/src/lib/services/association-settings.ts b/src/lib/services/association-settings.ts
index a998b1f..e2525db 100644
--- a/src/lib/services/association-settings.ts
+++ b/src/lib/services/association-settings.ts
@@ -94,6 +94,7 @@ export async function getAssociationSettings() {
settings.recaptchaEnabled &&
Boolean(settings.recaptchaSiteKey) &&
Boolean(settings.encryptedRecaptchaSecret),
+ whatsappGroupUrl: settings.whatsappGroupUrl,
legacyEnvironment: false,
updatedAt: settings.updatedAt,
};
@@ -122,6 +123,9 @@ export async function getAssociationSettings() {
recaptchaSecretConfigured: false,
recaptchaMinScore: 50,
recaptchaReady: false,
+ // La branche de repli doit avoir la même forme que l'autre, sinon le champ
+ // devient `string | null | undefined` chez tous les appelants.
+ whatsappGroupUrl: null,
legacyEnvironment: Boolean(
process.env.NEXT_PUBLIC_ASSO_NAME ||
process.env.NEXT_PUBLIC_SCHOOL_NAME ||
@@ -258,6 +262,8 @@ export async function saveAssociationSettings(
recaptchaSiteKey,
encryptedRecaptchaSecret,
recaptchaMinScore: data.recaptchaMinScore,
+ // Déjà normalisé par le schéma : "" est devenu null.
+ whatsappGroupUrl: data.whatsappGroupUrl ?? null,
updatedBy: actor.userId,
updatedAt: new Date(),
};
@@ -281,6 +287,10 @@ export async function saveAssociationSettings(
telegramTokenCleared: data.clearTelegramBotToken,
taskReminderWindowDays: data.taskReminderWindowDays,
volunteerReminderWindowDays: data.volunteerReminderWindowDays,
+ // Un booléen, jamais l'adresse : le journal d'audit n'a pas à porter de
+ // lien d'invitation.
+ whatsappGroupChanged:
+ (data.whatsappGroupUrl ?? null) !== (current?.whatsappGroupUrl ?? null),
},
);
diff --git a/src/lib/services/events.ts b/src/lib/services/events.ts
index 5b34e83..e92ae69 100644
--- a/src/lib/services/events.ts
+++ b/src/lib/services/events.ts
@@ -212,6 +212,10 @@ export async function duplicateEvent(
title: emptyToNull(title ?? null) ?? source.title,
description: source.description,
publicDescription: source.publicDescription,
+ // La billetterie n'est PAS recopiée : elle est propre à une édition, et
+ // une copie qui pointe vers la vente de l'an dernier encaisserait pour
+ // le mauvais événement.
+ ticketingUrl: null,
location: source.location,
startAt,
endAt: shift(source.endAt),
diff --git a/src/lib/ticketing.ts b/src/lib/ticketing.ts
new file mode 100644
index 0000000..f44f7ca
--- /dev/null
+++ b/src/lib/ticketing.ts
@@ -0,0 +1,124 @@
+/**
+ * Lien de billetterie d'un événement (HelloAsso le plus souvent).
+ *
+ * Module volontairement pur — aucun import serveur — pour que la même règle
+ * serve au formulaire d'un organisateur et à la validation de l'API. Sans ce
+ * partage, le message affiché finirait par ne plus décrire la règle appliquée.
+ *
+ * Le champ n'est pas réservé à HelloAsso : une APEL peut vendre ses places sur
+ * Billetweb, sur le site de la mairie ou via un formulaire de l'OGEC. On n'est
+ * sévère que là où l'on sait de quoi on parle — c'est-à-dire sur helloasso.com,
+ * où l'on sait distinguer une billetterie d'un espace d'administration.
+ */
+
+export const TICKETING_URL_MAX = 2000;
+
+export type TicketingUrlError =
+ | "illisible"
+ | "protocole"
+ | "administration"
+ | "accueil"
+ | "longueur";
+
+export const TICKETING_URL_MESSAGES: Record = {
+ illisible:
+ "Cette adresse n’est pas reconnue. Ouvrez votre billetterie dans le navigateur et copiez l’adresse complète depuis la barre du haut : elle commence par https:// et ressemble à https://www.helloasso.com/associations/…/evenements/…",
+ protocole:
+ "Le lien doit commencer par https:// et non http:// — les familles vont y saisir un moyen de paiement. Recopiez l’adresse depuis votre billetterie plutôt que de la retaper.",
+ administration:
+ "Ce lien mène à votre espace d’administration HelloAsso : les familles n’y verraient qu’une page de connexion. Ouvrez votre billetterie dans une fenêtre de navigation privée — l’adresse affichée est celle à coller ici.",
+ accueil:
+ "Ce lien pointe vers l’accueil de HelloAsso, pas vers votre billetterie. Depuis votre page HelloAsso, ouvrez la campagne concernée et copiez l’adresse qui s’affiche alors.",
+ longueur:
+ "Cette adresse est anormalement longue. Vérifiez que vous avez collé un lien et non un bloc de code d’intégration (une balise iframe, par exemple) ; dans ce cas, seule l’adresse entre guillemets est nécessaire.",
+};
+
+export type TicketingUrlCheck =
+ | { ok: true; url: string | null }
+ | { ok: false; raison: TicketingUrlError; message: string };
+
+/** Sous-domaines HelloAsso qu'une famille ne peut pas utiliser. */
+const SOUS_DOMAINES_INTERNES = ["admin", "auth", "api"];
+
+/**
+ * Un nom d'hôte plausible : au moins un point, et rien d'autre que des
+ * étiquettes valides. Sans ce contrôle, `new URL()` accepte « pas une adresse »
+ * en percent-encodant les espaces, et l'organisateur croit son lien enregistré.
+ */
+function hoteVraisemblable(hostname: string): boolean {
+ return /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)+$/.test(
+ hostname,
+ );
+}
+
+function estHelloAsso(hostname: string): boolean {
+ // Comparaison stricte, jamais un `includes` : « helloasso.com.piege.fr »
+ // contient la chaîne sans être HelloAsso.
+ return hostname === "helloasso.com" || hostname.endsWith(".helloasso.com");
+}
+
+/**
+ * Vérifie une saisie et rend l'adresse normalisée à stocker.
+ *
+ * Une chaîne vide n'est pas une erreur : le champ est facultatif, et la plupart
+ * des événements d'une APEL n'ont pas de billetterie.
+ */
+export function checkTicketingUrl(saisie: string | null | undefined): TicketingUrlCheck {
+ const brut = (saisie ?? "").trim();
+ if (!brut) return { ok: true, url: null };
+ if (brut.length > TICKETING_URL_MAX) {
+ return { ok: false, raison: "longueur", message: TICKETING_URL_MESSAGES.longueur };
+ }
+
+ // La barre d'adresse de Chrome copie « www.helloasso.com/… » sans schéma :
+ // le refuser pour cette seule raison serait absurde.
+ const candidat = /^[a-z][a-z0-9+.-]*:/i.test(brut) ? brut : `https://${brut}`;
+
+ let url: URL;
+ try {
+ url = new URL(candidat);
+ } catch {
+ return { ok: false, raison: "illisible", message: TICKETING_URL_MESSAGES.illisible };
+ }
+
+ if (!hoteVraisemblable(url.hostname)) {
+ return { ok: false, raison: "illisible", message: TICKETING_URL_MESSAGES.illisible };
+ }
+
+ if (url.protocol !== "https:") {
+ // http: comme javascript: tombent ici. On refuse au lieu de réécrire : une
+ // adresse en http est le plus souvent une adresse retapée à la main.
+ return { ok: false, raison: "protocole", message: TICKETING_URL_MESSAGES.protocole };
+ }
+
+ if (estHelloAsso(url.hostname)) {
+ const sousDomaine = url.hostname.split(".")[0];
+ if (SOUS_DOMAINES_INTERNES.includes(sousDomaine)) {
+ return {
+ ok: false,
+ raison: "administration",
+ message: TICKETING_URL_MESSAGES.administration,
+ };
+ }
+ const segments = url.pathname.split("/").filter(Boolean);
+ if (segments.length < 2) {
+ return { ok: false, raison: "accueil", message: TICKETING_URL_MESSAGES.accueil };
+ }
+ }
+
+ return { ok: true, url: url.toString() };
+}
+
+/**
+ * Nom du service à annoncer aux familles : « sur HelloAsso » rassure, « sur
+ * un autre site » inquiéterait. Rend null si l'adresse est illisible.
+ */
+export function ticketingHostLabel(url: string | null | undefined): string | null {
+ if (!url) return null;
+ try {
+ const hostname = new URL(url).hostname.replace(/^www\./, "");
+ return estHelloAsso(hostname) ? "HelloAsso" : hostname;
+ } catch {
+ return null;
+ }
+}
diff --git a/src/lib/validation.ts b/src/lib/validation.ts
index d6c0029..4093f00 100644
--- a/src/lib/validation.ts
+++ b/src/lib/validation.ts
@@ -2,6 +2,8 @@ import { z } from "zod";
import { parseLocalDateTime } from "@/lib/dates";
import { ASSOCIATION_DOCUMENT_TYPES } from "@/lib/labels";
+import { checkTicketingUrl, TICKETING_URL_MAX } from "@/lib/ticketing";
+import { checkWhatsappUrl, WHATSAPP_URL_MAX } from "@/lib/whatsapp";
import {
LEAD_TIME_MAX,
type LeadTimeUnit,
@@ -47,6 +49,20 @@ export const eventSchema = z.object({
/** Réservé à l'équipe. Ce qui doit être lu par les visiteurs va dans `publicDescription`. */
description: z.string().max(5000).optional(),
publicDescription: z.string().max(5000).optional(),
+ /** Billetterie en ligne : validée par la règle partagée avec le formulaire. */
+ ticketingUrl: z
+ .string()
+ .max(TICKETING_URL_MAX)
+ .optional()
+ .nullable()
+ .transform((valeur, ctx) => {
+ const verdict = checkTicketingUrl(valeur);
+ if (!verdict.ok) {
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: verdict.message });
+ return z.NEVER;
+ }
+ return verdict.url;
+ }),
location: z.string().max(300).optional(),
startAt: localDateTime,
endAt: localDateTime.nullable().optional(),
@@ -390,6 +406,20 @@ export const associationSettingsSchema = z.object({
clearRecaptchaSecret: z.boolean().default(false),
/** Note minimale acceptée, en pourcentage : Google note de 0 à 1. */
recaptchaMinScore: z.coerce.number().int().min(0).max(100).default(50),
+ /** Lien d'invitation du groupe WhatsApp, publié tel quel sur le site. */
+ whatsappGroupUrl: z
+ .string()
+ .max(WHATSAPP_URL_MAX)
+ .optional()
+ .nullable()
+ .transform((valeur, ctx) => {
+ const verdict = checkWhatsappUrl(valeur);
+ if (!verdict.ok) {
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: verdict.message });
+ return z.NEVER;
+ }
+ return verdict.url;
+ }),
});
/**
diff --git a/src/lib/whatsapp.ts b/src/lib/whatsapp.ts
new file mode 100644
index 0000000..2776214
--- /dev/null
+++ b/src/lib/whatsapp.ts
@@ -0,0 +1,102 @@
+/**
+ * Lien d'invitation au groupe WhatsApp de l'association.
+ *
+ * Module pur — aucun import serveur — partagé par le formulaire de
+ * configuration et par la validation de l'API, pour que le message affiché à
+ * l'administrateur décrive bien la règle appliquée.
+ *
+ * Le lien est destiné à être publié : ce n'est pas un secret, il n'est ni
+ * chiffré ni masqué. En revanche, il ouvre l'entrée du groupe à quiconque
+ * trouve la page — l'aide du champ le dit explicitement.
+ */
+
+export const WHATSAPP_URL_MAX = 500;
+
+export type WhatsappUrlError = "illisible" | "protocole" | "conversation" | "longueur";
+
+export const WHATSAPP_URL_MESSAGES: Record = {
+ illisible:
+ "Cette adresse n’est pas reconnue. Dans WhatsApp : Infos du groupe › Inviter via un lien › Copier le lien, puis collez-le ici — il ressemble à https://chat.whatsapp.com/…",
+ protocole:
+ "Le lien doit commencer par https:// — recopiez-le depuis WhatsApp plutôt que de le retaper.",
+ conversation:
+ "Ce lien ouvre une conversation avec une personne, pas un groupe. Utilisez le lien d’invitation du groupe : Infos du groupe › Inviter via un lien.",
+ longueur:
+ "Cette adresse est anormalement longue. Vérifiez que vous avez collé un lien et non un bloc de code.",
+};
+
+export type WhatsappUrlCheck =
+ | { ok: true; url: string | null }
+ | { ok: false; raison: WhatsappUrlError; message: string };
+
+/** Adresses WhatsApp qui ouvrent une discussion individuelle, pas un groupe. */
+const HOTES_CONVERSATION = ["wa.me", "api.whatsapp.com"];
+
+/**
+ * Un nom d'hôte plausible : au moins un point, et rien d'autre que des
+ * étiquettes valides. Sans ce contrôle, `new URL()` accepte « pas une adresse »
+ * en percent-encodant les espaces, et l'organisateur croit son lien enregistré.
+ */
+function hoteVraisemblable(hostname: string): boolean {
+ return /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)+$/.test(
+ hostname,
+ );
+}
+
+function estHote(hostname: string, hote: string): boolean {
+ return hostname === hote || hostname.endsWith(`.${hote}`);
+}
+
+/**
+ * Vérifie une saisie et rend l'adresse normalisée à stocker.
+ *
+ * On n'est sévère que là où l'on sait de quoi on parle : une adresse WhatsApp
+ * qui n'est manifestement pas celle d'un groupe est refusée, mais un lien
+ * raccourci ou une redirection maison passe — l'association reste maîtresse de
+ * ce qu'elle publie.
+ */
+export function checkWhatsappUrl(saisie: string | null | undefined): WhatsappUrlCheck {
+ const brut = (saisie ?? "").trim();
+ if (!brut) return { ok: true, url: null };
+ if (brut.length > WHATSAPP_URL_MAX) {
+ return { ok: false, raison: "longueur", message: WHATSAPP_URL_MESSAGES.longueur };
+ }
+
+ const candidat = /^[a-z][a-z0-9+.-]*:/i.test(brut) ? brut : `https://${brut}`;
+ let url: URL;
+ try {
+ url = new URL(candidat);
+ } catch {
+ return { ok: false, raison: "illisible", message: WHATSAPP_URL_MESSAGES.illisible };
+ }
+
+ if (!hoteVraisemblable(url.hostname)) {
+ return { ok: false, raison: "illisible", message: WHATSAPP_URL_MESSAGES.illisible };
+ }
+
+ if (url.protocol !== "https:") {
+ return { ok: false, raison: "protocole", message: WHATSAPP_URL_MESSAGES.protocole };
+ }
+ if (url.username || url.password) {
+ return { ok: false, raison: "illisible", message: WHATSAPP_URL_MESSAGES.illisible };
+ }
+ if (HOTES_CONVERSATION.some((hote) => estHote(url.hostname, hote))) {
+ return {
+ ok: false,
+ raison: "conversation",
+ message: WHATSAPP_URL_MESSAGES.conversation,
+ };
+ }
+
+ return { ok: true, url: url.toString() };
+}
+
+/** Vrai lien d'invitation de groupe : sert à prévenir sans bloquer. */
+export function estInvitationGroupe(url: string | null | undefined): boolean {
+ if (!url) return false;
+ try {
+ return new URL(url).hostname === "chat.whatsapp.com";
+ } catch {
+ return false;
+ }
+}