From 6e90ea2b70e0ea271e087434a2dbc87641a2786e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 02:01:16 +0000 Subject: [PATCH 1/3] =?UTF-8?q?Refond=20la=20page=20=C2=AB=20Rejoindre=20?= =?UTF-8?q?=C2=BB=20et=20ouvre=20un=20champ=20billetterie=20par=20=C3=A9v?= =?UTF-8?q?=C3=A9nement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La page Rejoindre affirmait sans rien montrer, et ne menait nulle part : l'ancre #contact existait mais aucun lien du site n'y conduisait, si bien que le formulaire — seul point de conversion — n'était atteignable qu'en faisant défiler trois sections. Sa colonne de 896 px ne s'alignait ni sur l'en-tête ni sur le pied de page, d'où l'impression d'une page de mentions légales. Elle repart de ce qui retient vraiment un parent : quatre objections énoncées à voix haute puis désamorcées, avant toute demande. Vient ensuite ce qui prouve que l'association existe — les rendez-vous réellement publiés, avec les places qui restent, et la porte la plus basse qui soit : prendre un créneau sans compte. Le formulaire arrive en dernier, précédé de ce qui se passe après l'envoi. Le héros aligné sur max-w-7xl, deux boutons, une carte montrant le prochain rendez-vous et les chiffres du moment, une respiration colorée, des missions compactées en tuiles : la beauté vient de la typographie et de la couleur, faute de photographie. Tout reste calculé côté serveur : getUpcomingPublishedEvents rapporte aussi les inscriptions des bénévoles, jetons d'annulation compris, qui n'ont rien à faire dans le flux d'une page publique indexable. S'y ajoute le début de la billetterie en ligne : un lien par événement, saisi par l'organisateur. Le champ n'est pas réservé à HelloAsso — une APEL peut vendre ses places ailleurs — mais quand l'adresse est bien HelloAsso, elle est vérifiée : un lien vers l'espace d'administration, l'erreur la plus fréquente, ne montrerait aux familles qu'un écran de connexion. Une copie d'événement ne reprend jamais la billetterie de l'édition précédente. La présentation publique de ce lien suit, avec le bloc WhatsApp. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014SfQYBU4xXTeSEHKhHQXdD --- drizzle/0017_event_ticketing_url.sql | 1 + drizzle/meta/0017_snapshot.json | 3360 ++++++++++++++++++++++++++ drizzle/meta/_journal.json | 7 + src/app/api/events/[id]/route.ts | 5 + src/app/api/events/route.ts | 1 + src/app/rejoindre/page.tsx | 747 +++++- src/components/event-form.tsx | 76 +- src/lib/db/schema.ts | 7 + src/lib/mcp/register-core-tools.ts | 7 + src/lib/services/events.ts | 4 + src/lib/ticketing.ts | 109 + src/lib/validation.ts | 15 + 12 files changed, 4224 insertions(+), 115 deletions(-) create mode 100644 drizzle/0017_event_ticketing_url.sql create mode 100644 drizzle/meta/0017_snapshot.json create mode 100644 src/lib/ticketing.ts 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/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/_journal.json b/drizzle/meta/_journal.json index ee3d7de..aff17da 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -120,6 +120,13 @@ "when": 1787454647327, "tag": "0016_push_notifications", "breakpoints": true + }, + { + "idx": 17, + "version": "7", + "when": 1788055218908, + "tag": "0017_event_ticketing_url", + "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/rejoindre/page.tsx b/src/app/rejoindre/page.tsx index 9c5237c..ca0bff8 100644 --- a/src/app/rejoindre/page.tsx +++ b/src/app/rejoindre/page.tsx @@ -1,27 +1,46 @@ 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 { 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 +48,667 @@ 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 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 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 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 ————— */}
-
-
-

- Ce que fait l’association + {/* ————— Respiration : une phrase, un aplat, aucune demande ————— */} +

+
+ + {/* ————— 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. +

+
-
-
-
-
- -
+ {/* ————— Le coupon-réponse : ce qui se passe après l'envoi, puis le formulaire ————— */}
-
-

- 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 ?{" "} - - - - )} -

-
- +
+
+ + +
+ {contactEmail ? ( + <> +

+ Écrire à {settings.associationName} +

+

+ Quelques mots suffisent. Vous pouvez aussi simplement dire + bonjour. +

+ +
+

+ Si vous ne savez pas quoi écrire, recopiez une de ces + phrases +

+
    + {AMORCES.map((amorce) => ( +
  • +
  • + ))} +
+
+ +
+ +
+ + ) : ( + <> +

+ Le formulaire n’est pas disponible pour l’instant +

+

+ L’association n’a pas encore publié d’adresse de contact : + votre message ne partirait nulle part. Repassez d’ici + quelques jours, ou parlez-en à un parent de l’équipe à la + sortie de l’école. +

+ + )} +
+ + + + Retour à l’accueil +
diff --git a/src/components/event-form.tsx b/src/components/event-form.tsx index 9c75329..66777bc 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,57 @@ export function EventForm({ +
+
+
+

+ 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. +

+ { + 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 +273,7 @@ export function EventForm({ {!editing && templates.length > 0 && ( diff --git a/src/lib/db/schema.ts b/src/lib/db/schema.ts index 94257cc..73ac31e 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 }), 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/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..849865c --- /dev/null +++ b/src/lib/ticketing.ts @@ -0,0 +1,109 @@ +/** + * 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"]; + +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 (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..32c068f 100644 --- a/src/lib/validation.ts +++ b/src/lib/validation.ts @@ -2,6 +2,7 @@ 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 { LEAD_TIME_MAX, type LeadTimeUnit, @@ -47,6 +48,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(), From 40f71de3b749c59f5b1f0901fa2e79148a91c673 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 02:14:59 +0000 Subject: [PATCH 2/3] Annonce le groupe WhatsApp de l'association MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Les familles suivent déjà l'association par WhatsApp ; le site n'en disait rien. Un carton d'invitation le dit maintenant sur la page « Rejoindre », avec un aperçu de ce qui circule dans le groupe : les dates, les changements de dernière minute, les coups de main cherchés. L'aperçu est une illustration, et il est cadré comme telle. Sa légende « Exemple de ce qui circule dans le groupe » est visible et lue avant les bulles, y compris par un lecteur d'écran : ni horodatage, ni double coche, ni prénom, rien qui puisse passer pour une conversation réelle. Un jury a écarté pour cette raison la maquette de notification, plus spectaculaire mais qui présentait comme reçu un message que l'association n'a jamais envoyé. 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. Ailleurs, un seul lien en texte, jamais le carton : dans le pied de page de tout le site public, une ligne sur l'accueil, et une phrase dans les deux écrans de confirmation — après avoir écrit, et après avoir pris un créneau, où le besoin est le plus concret. Le lien se règle dans Configuration ; sans lui, ni carton, ni lien de pied de page, ni mention nulle part. Le dépôt est public et l'application sert plusieurs associations : aucune invitation ne peut être écrite en dur, et le champ dit clairement que le lien publié ouvre le groupe à quiconque trouve la page. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014SfQYBU4xXTeSEHKhHQXdD --- drizzle/0018_whatsapp_group_link.sql | 1 + drizzle/meta/0018_snapshot.json | 3366 ++++++++++++++++++ drizzle/meta/_journal.json | 7 + src/app/dashboard/settings/page.tsx | 1 + src/app/inscription/[token]/page.tsx | 1 + src/app/page.tsx | 15 + src/app/rejoindre/page.tsx | 14 + src/components/association-settings-form.tsx | 79 + src/components/join-form.tsx | 19 + src/components/site-footer.tsx | 14 +- src/components/volunteer-signup-form.tsx | 18 + src/components/whatsapp-invite-card.tsx | 150 + src/lib/db/schema.ts | 6 + src/lib/services/association-settings.ts | 10 + src/lib/validation.ts | 15 + src/lib/whatsapp.ts | 87 + 16 files changed, 3802 insertions(+), 1 deletion(-) create mode 100644 drizzle/0018_whatsapp_group_link.sql create mode 100644 drizzle/meta/0018_snapshot.json create mode 100644 src/components/whatsapp-invite-card.tsx create mode 100644 src/lib/whatsapp.ts 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/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 aff17da..f876cc7 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -127,6 +127,13 @@ "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/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..aa766b5 100644 --- a/src/app/inscription/[token]/page.tsx +++ b/src/app/inscription/[token]/page.tsx @@ -202,6 +202,7 @@ export default async function InscriptionPage({ } defaultName={currentUser?.name ?? ""} defaultEmail={currentUser?.email ?? ""} + whatsappGroupUrl={association.whatsappGroupUrl} /> )} diff --git a/src/app/page.tsx b/src/app/page.tsx index 1f548fe..8d71b93 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -5,6 +5,7 @@ import { Hand, HeartHandshake, MapPin, + MessageCircle, MousePointerClick, } from "lucide-react"; import Link from "next/link"; @@ -333,6 +334,20 @@ export default async function HomePage() { ), )}
+ + {settings.whatsappGroupUrl && ( + + + )} diff --git a/src/app/rejoindre/page.tsx b/src/app/rejoindre/page.tsx index ca0bff8..218ea38 100644 --- a/src/app/rejoindre/page.tsx +++ b/src/app/rejoindre/page.tsx @@ -17,6 +17,7 @@ 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"; @@ -579,6 +580,18 @@ export default async function RejoindrePage() { + {/* ————— Le groupe WhatsApp : le canal où tout se suit au fil de l'eau ————— */} + {settings.whatsappGroupUrl && ( +
+
+ +
+
+ )} + {/* ————— Le coupon-réponse : ce qui se passe après l'envoi, puis le formulaire ————— */}
@@ -683,6 +696,7 @@ export default async function RejoindrePage() { recaptchaSiteKey={ settings.recaptchaReady ? settings.recaptchaSiteKey : null } + whatsappGroupUrl={settings.whatsappGroupUrl} />
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/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 && ( + + + )} s.remaining > 0); @@ -51,6 +54,21 @@ export function VolunteerSignupForm({

À très bientôt pour donner un coup de main. ⚓

+ {whatsappGroupUrl && ( +

+ Les rappels et les changements de dernière minute passent par le{" "} + + groupe WhatsApp de l’association + (ouvre WhatsApp dans un nouvel onglet) + + . +

+ )}
); @@ -191,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. */} ); 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/ticketing.ts b/src/lib/ticketing.ts index 849865c..f44f7ca 100644 --- a/src/lib/ticketing.ts +++ b/src/lib/ticketing.ts @@ -40,6 +40,17 @@ export type TicketingUrlCheck = /** 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. @@ -70,6 +81,10 @@ export function checkTicketingUrl(saisie: string | null | undefined): TicketingU 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. diff --git a/src/lib/whatsapp.ts b/src/lib/whatsapp.ts index 1cf2a59..2776214 100644 --- a/src/lib/whatsapp.ts +++ b/src/lib/whatsapp.ts @@ -32,6 +32,17 @@ export type WhatsappUrlCheck = /** 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}`); } @@ -59,6 +70,10 @@ export function checkWhatsappUrl(saisie: string | null | undefined): WhatsappUrl 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 }; }