diff --git a/drizzle/0018_lazy_big_bertha.sql b/drizzle/0018_lazy_big_bertha.sql new file mode 100644 index 0000000..e03f68e --- /dev/null +++ b/drizzle/0018_lazy_big_bertha.sql @@ -0,0 +1,27 @@ +CREATE TYPE "public"."info_status" AS ENUM('draft', 'published');--> statement-breakpoint +CREATE TABLE "information_reads" ( + "id" serial PRIMARY KEY NOT NULL, + "information_id" integer NOT NULL, + "user_id" integer NOT NULL, + "read_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "informations" ( + "id" serial PRIMARY KEY NOT NULL, + "title" varchar(200) NOT NULL, + "body" text NOT NULL, + "image_url" text, + "status" "info_status" DEFAULT 'draft' NOT NULL, + "pinned" boolean DEFAULT false NOT NULL, + "published_at" timestamp with time zone, + "author_id" integer, + "email_sent_at" timestamp with time zone, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "information_reads" ADD CONSTRAINT "information_reads_information_id_informations_id_fk" FOREIGN KEY ("information_id") REFERENCES "public"."informations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "information_reads" ADD CONSTRAINT "information_reads_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "informations" ADD CONSTRAINT "informations_author_id_users_id_fk" FOREIGN KEY ("author_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "information_reads_user_info_idx" ON "information_reads" USING btree ("user_id","information_id");--> statement-breakpoint +CREATE INDEX "informations_feed_idx" ON "informations" USING btree ("status","published_at"); \ 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..4960473 --- /dev/null +++ b/drizzle/meta/0018_snapshot.json @@ -0,0 +1,1647 @@ +{ + "id": "e80a2f6a-ea01-4d00-ab3d-7472b909ff82", + "prevId": "e64a4fd6-108b-4300-889a-2a4482c0db79", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.activity_log": { + "name": "activity_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "dot": { + "name": "dot", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true, + "default": "'#2C6FB3'" + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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.categories": { + "name": "categories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(80)", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar(120)", + "primaryKey": false, + "notNull": true + }, + "accent": { + "name": "accent", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true, + "default": "'#E0A63C'" + }, + "tint": { + "name": "tint", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true, + "default": "'#f6efdc'" + }, + "sort": { + "name": "sort", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "categories_slug_unique": { + "name": "categories_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contact_messages": { + "name": "contact_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "contact_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'new'" + }, + "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.image_consents": { + "name": "image_consents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "member_id": { + "name": "member_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "decision": { + "name": "decision", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "signatory_name": { + "name": "signatory_name", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "signature_png": { + "name": "signature_png", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consent_version": { + "name": "consent_version", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "ip": { + "name": "ip", + "type": "varchar(120)", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "image_consents_member_id_members_id_fk": { + "name": "image_consents_member_id_members_id_fk", + "tableFrom": "image_consents", + "tableTo": "members", + "columnsFrom": [ + "member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.information_reads": { + "name": "information_reads", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "information_id": { + "name": "information_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "read_at": { + "name": "read_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "information_reads_user_info_idx": { + "name": "information_reads_user_info_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "information_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "information_reads_information_id_informations_id_fk": { + "name": "information_reads_information_id_informations_id_fk", + "tableFrom": "information_reads", + "tableTo": "informations", + "columnsFrom": [ + "information_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "information_reads_user_id_users_id_fk": { + "name": "information_reads_user_id_users_id_fk", + "tableFrom": "information_reads", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.informations": { + "name": "informations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "image_url": { + "name": "image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "info_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "pinned": { + "name": "pinned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "author_id": { + "name": "author_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "email_sent_at": { + "name": "email_sent_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "informations_feed_idx": { + "name": "informations_feed_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "published_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "informations_author_id_users_id_fk": { + "name": "informations_author_id_users_id_fk", + "tableFrom": "informations", + "tableTo": "users", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.meeting_registrations": { + "name": "meeting_registrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "meeting_id": { + "name": "meeting_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "member_id": { + "name": "member_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "attendee_name": { + "name": "attendee_name", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "attendee_company": { + "name": "attendee_company", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "attendee_email": { + "name": "attendee_email", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "attendee_phone": { + "name": "attendee_phone", + "type": "varchar(40)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "image_consent": { + "name": "image_consent", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "meeting_registrations_meeting_idx": { + "name": "meeting_registrations_meeting_idx", + "columns": [ + { + "expression": "meeting_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "meeting_registrations_member_meeting_idx": { + "name": "meeting_registrations_member_meeting_idx", + "columns": [ + { + "expression": "member_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "meeting_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "meeting_registrations_meeting_id_meetings_id_fk": { + "name": "meeting_registrations_meeting_id_meetings_id_fk", + "tableFrom": "meeting_registrations", + "tableTo": "meetings", + "columnsFrom": [ + "meeting_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "meeting_registrations_member_id_members_id_fk": { + "name": "meeting_registrations_member_id_members_id_fk", + "tableFrom": "meeting_registrations", + "tableTo": "members", + "columnsFrom": [ + "member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.meetings": { + "name": "meetings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "starts_at": { + "name": "starts_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "location": { + "name": "location", + "type": "varchar(240)", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "capacity": { + "name": "capacity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 30 + }, + "participants_per_account": { + "name": "participants_per_account", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "image_url": { + "name": "image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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.members": { + "name": "members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "contact_email": { + "name": "contact_email", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "contact_first_name": { + "name": "contact_first_name", + "type": "varchar(120)", + "primaryKey": false, + "notNull": false + }, + "contact_last_name": { + "name": "contact_last_name", + "type": "varchar(120)", + "primaryKey": false, + "notNull": false + }, + "contact_phone": { + "name": "contact_phone", + "type": "varchar(40)", + "primaryKey": false, + "notNull": false + }, + "category_id": { + "name": "category_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "city": { + "name": "city", + "type": "varchar(120)", + "primaryKey": false, + "notNull": false + }, + "address": { + "name": "address", + "type": "varchar(240)", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "highlighted": { + "name": "highlighted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cover_url": { + "name": "cover_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "varchar(40)", + "primaryKey": false, + "notNull": false + }, + "website": { + "name": "website", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "postal_code": { + "name": "postal_code", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "member_since": { + "name": "member_since", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "hours": { + "name": "hours", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "members_category_id_categories_id_fk": { + "name": "members_category_id_categories_id_fk", + "tableFrom": "members", + "tableTo": "categories", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.membership_requests": { + "name": "membership_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "request_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'new'" + }, + "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.past_meeting_photos": { + "name": "past_meeting_photos", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "past_meeting_id": { + "name": "past_meeting_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "image_url": { + "name": "image_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "caption": { + "name": "caption", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "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": { + "past_meeting_photos_past_meeting_id_past_meetings_id_fk": { + "name": "past_meeting_photos_past_meeting_id_past_meetings_id_fk", + "tableFrom": "past_meeting_photos", + "tableTo": "past_meetings", + "columnsFrom": [ + "past_meeting_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.past_meetings": { + "name": "past_meetings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "event_date": { + "name": "event_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "location": { + "name": "location", + "type": "varchar(240)", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "participants": { + "name": "participants", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "meeting_id": { + "name": "meeting_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "past_meetings_meeting_id_meetings_id_fk": { + "name": "past_meetings_meeting_id_meetings_id_fk", + "tableFrom": "past_meetings", + "tableTo": "meetings", + "columnsFrom": [ + "meeting_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.promotions": { + "name": "promotions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "varchar(120)", + "primaryKey": false, + "notNull": false + }, + "badge": { + "name": "badge", + "type": "varchar(40)", + "primaryKey": false, + "notNull": false + }, + "image_url": { + "name": "image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "member_id": { + "name": "member_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "promo_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "starts_on": { + "name": "starts_on", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "ends_on": { + "name": "ends_on", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "valid_until": { + "name": "valid_until", + "type": "varchar(120)", + "primaryKey": false, + "notNull": false + }, + "share_facebook": { + "name": "share_facebook", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "share_linkedin": { + "name": "share_linkedin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "publish_at": { + "name": "publish_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "suspended_by": { + "name": "suspended_by", + "type": "promo_suspended_by", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "suspended_by_id": { + "name": "suspended_by_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "suspended_at": { + "name": "suspended_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": { + "promotions_member_id_members_id_fk": { + "name": "promotions_member_id_members_id_fk", + "tableFrom": "promotions", + "tableTo": "members", + "columnsFrom": [ + "member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "promotions_suspended_by_id_users_id_fk": { + "name": "promotions_suspended_by_id_users_id_fk", + "tableFrom": "promotions", + "tableTo": "users", + "columnsFrom": [ + "suspended_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.site_settings": { + "name": "site_settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "varchar(120)", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.social_accounts": { + "name": "social_accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "network": { + "name": "network", + "type": "social_network", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "app_id": { + "name": "app_id", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "app_secret": { + "name": "app_secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "target_id": { + "name": "target_id", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "target_name": { + "name": "target_name", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "connected_by_id": { + "name": "connected_by_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "connected_at": { + "name": "connected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_check_at": { + "name": "last_check_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_check_ok": { + "name": "last_check_ok", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "last_check_error": { + "name": "last_check_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "social_accounts_connected_by_id_users_id_fk": { + "name": "social_accounts_connected_by_id_users_id_fk", + "tableFrom": "social_accounts", + "tableTo": "users", + "columnsFrom": [ + "connected_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "social_accounts_network_unique": { + "name": "social_accounts_network_unique", + "nullsNotDistinct": false, + "columns": [ + "network" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.social_posts": { + "name": "social_posts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "promotion_id": { + "name": "promotion_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "network": { + "name": "network", + "type": "social_network", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "social_post_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "varchar(200)", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "posted_by_id": { + "name": "posted_by_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "social_posts_promotion_idx": { + "name": "social_posts_promotion_idx", + "columns": [ + { + "expression": "promotion_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "social_posts_promotion_id_promotions_id_fk": { + "name": "social_posts_promotion_id_promotions_id_fk", + "tableFrom": "social_posts", + "tableTo": "promotions", + "columnsFrom": [ + "promotion_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "social_posts_posted_by_id_users_id_fk": { + "name": "social_posts_posted_by_id_users_id_fk", + "tableFrom": "social_posts", + "tableTo": "users", + "columnsFrom": [ + "posted_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(200)", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "member_id": { + "name": "member_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "must_change_password": { + "name": "must_change_password", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "session_version": { + "name": "session_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": { + "users_email_idx": { + "name": "users_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "users_member_id_members_id_fk": { + "name": "users_member_id_members_id_fk", + "tableFrom": "users", + "tableTo": "members", + "columnsFrom": [ + "member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.contact_status": { + "name": "contact_status", + "schema": "public", + "values": [ + "new", + "read", + "archived" + ] + }, + "public.info_status": { + "name": "info_status", + "schema": "public", + "values": [ + "draft", + "published" + ] + }, + "public.member_status": { + "name": "member_status", + "schema": "public", + "values": [ + "active", + "pending" + ] + }, + "public.promo_status": { + "name": "promo_status", + "schema": "public", + "values": [ + "pending", + "live", + "expired", + "rejected", + "suspended", + "scheduled" + ] + }, + "public.promo_suspended_by": { + "name": "promo_suspended_by", + "schema": "public", + "values": [ + "member", + "staff" + ] + }, + "public.request_status": { + "name": "request_status", + "schema": "public", + "values": [ + "new", + "approved", + "rejected" + ] + }, + "public.role": { + "name": "role", + "schema": "public", + "values": [ + "admin", + "moderator", + "editor", + "member" + ] + }, + "public.social_network": { + "name": "social_network", + "schema": "public", + "values": [ + "facebook", + "linkedin" + ] + }, + "public.social_post_status": { + "name": "social_post_status", + "schema": "public", + "values": [ + "posted", + "failed" + ] + } + }, + "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 de31eed..86e7adc 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -127,6 +127,13 @@ "when": 1789385157231, "tag": "0017_regular_devos", "breakpoints": true + }, + { + "idx": 18, + "version": "7", + "when": 1789399747132, + "tag": "0018_lazy_big_bertha", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/app/backend/BackendShell.tsx b/src/app/backend/BackendShell.tsx index c0f4fa0..324abba 100644 --- a/src/app/backend/BackendShell.tsx +++ b/src/app/backend/BackendShell.tsx @@ -19,6 +19,7 @@ const TITLES: Record = { "/backend/rencontres": ["Rencontres", "Gérer les prochaines dates"], "/backend/inscriptions": ["Inscriptions", "Gérer les participants aux rencontres"], "/backend/rencontres-passees": ["Rencontres passées", "Publier les archives et leurs photos"], + "/backend/informations": ["Informations", "Ce que l'association publie dans l'espace adhérent"], "/backend/emails": ["Création d'e-mails", "Composer des messages aux couleurs de Plein R"], "/backend/reseaux": ["Réseaux sociaux", "Connecter les pages Facebook et LinkedIn"], "/backend/administrateurs": ["Administrateurs", "Gérer les accès à l'administration"], @@ -26,11 +27,12 @@ const TITLES: Record = { "/backend/parametres": ["Paramètres du site", "Configurer l'association et les mentions légales"], "/backend/espace": ["Mon profil", "Votre fiche publique, vos inscriptions et votre droit à l'image"], "/backend/espace/promotions": ["Mes promotions", "Proposez une offre et suivez celles en ligne"], + "/backend/espace/informations": ["Informations", "Les annonces de l'association"], "/backend/changer-mot-de-passe": ["Mot de passe", "Sécurisez votre compte"], }; type Capability = Parameters[1]; -type BadgeKey = "inbox" | "promos"; +type BadgeKey = "inbox" | "promos" | "infos"; type NavItem = { href: string; @@ -72,6 +74,7 @@ const SECTIONS: NavSection[] = [ { label: "Communication", items: [ + { href: "/backend/informations", label: "Informations", icon: "news", capability: "manageInformations" }, { href: "/backend/emails", label: "E-mails", icon: "emails", capability: "manageEmails" }, { href: "/backend/reseaux", label: "Réseaux sociaux", icon: "social", capability: "manageSettings" }, ], @@ -87,6 +90,7 @@ const SECTIONS: NavSection[] = [ { label: "Côté adhérent", items: [ + { href: "/backend/espace/informations", label: "Informations", icon: "news", badge: "infos" }, { href: "/backend/espace", label: "Mon profil", icon: "space" }, { href: "/backend/espace/promotions", label: "Mes promotions", icon: "promos" }, ], @@ -124,11 +128,13 @@ export function BackendShell({ user, pendingCount, inboxCount = 0, + infoCount = 0, children, }: { user: { name: string; role: AppRole }; pendingCount: number; inboxCount?: number; + infoCount?: number; children: ReactNode; }) { const pathname = usePathname(); @@ -164,7 +170,7 @@ export function BackendShell({ return () => window.removeEventListener("keydown", onKey); }, [menuOpen]); - const badges: Record = { inbox: inboxCount, promos: pendingCount }; + const badges: Record = { inbox: inboxCount, promos: pendingCount, infos: infoCount }; const sections = SECTIONS.map((section) => ({ ...section, diff --git a/src/app/backend/actions.ts b/src/app/backend/actions.ts index 55143c9..0b9b262 100644 --- a/src/app/backend/actions.ts +++ b/src/app/backend/actions.ts @@ -16,6 +16,7 @@ import { categories, contactMessages, imageConsents, + informations, meetingRegistrations, meetings, members, @@ -42,6 +43,7 @@ import { isRangeInvalid } from "@/lib/promo-validity"; import { formatSchedule, isDue, parseScheduleInput } from "@/lib/promo-schedule"; import { publishPromoShares, requestedNetworks } from "@/lib/promo-publish"; import { logActivity } from "@/lib/activity-log"; +import { markInformationsRead as markRead } from "@/lib/informations"; import { SITE_SETTING_DEFAULTS } from "@/lib/site-settings"; import type { AppRole } from "@/types/next-auth"; @@ -1312,3 +1314,160 @@ export async function disconnectSocial(formData: FormData) { export async function doSignOut() { await signOut({ redirectTo: "/" }); } + +// ---- Informations de l'association ---- + +function revalidateInformationPaths() { + revalidatePath("/backend/informations"); + revalidatePath("/backend/espace"); + revalidatePath("/backend/espace/informations"); + revalidatePath("/backend/espace/promotions"); + revalidatePath("/backend"); +} + +async function requireInformations() { + const access = await requireRole(); + if (!can(access.role, "manageInformations")) throw new Error("Accès refusé"); + return access; +} + +/** + * Une seule information épinglée à la fois. Appliqué ici plutôt que par un + * index partiel : l'écriture est réservée au staff, il n'y a pas de course + * réelle, et drizzle-kit ne génère pas les index conditionnels. + */ +async function unpinOthers(keepId: number) { + await db + .update(informations) + .set({ pinned: false }) + .where(and(eq(informations.pinned, true), ne(informations.id, keepId))); +} + +export type SavedInformation = { id: number }; + +export async function saveInformation( + formData: FormData +): Promise { + const { userId } = await requireInformations(); + + const id = Number(formData.get("id") ?? 0) || null; + const title = asString(formData, "title"); + const body = asString(formData, "body"); + if (!title) return { error: "Donnez un titre à cette information." }; + if (!body) return { error: "Le message est vide." }; + + let imageUrl: string | null = null; + try { + imageUrl = asImageDataUri(formData, "imageUrl"); + } catch (error) { + // Refus de saisie attendu : on le renvoie au formulaire, qui garde le reste. + return { error: error instanceof Error ? error.message : "Image refusée." }; + } + + const pinned = formData.get("pinned") === "on"; + const now = new Date(); + + if (id) { + const [updated] = await db + .update(informations) + .set({ title, body, imageUrl, pinned, updatedAt: now }) + .where(eq(informations.id, id)) + .returning({ id: informations.id }); + if (!updated) return { error: "Cette information n'existe plus." }; + if (pinned) await unpinOthers(updated.id); + revalidateInformationPaths(); + return { id: updated.id }; + } + + const [created] = await db + .insert(informations) + .values({ title, body, imageUrl, pinned, authorId: userId, createdAt: now, updatedAt: now }) + .returning({ id: informations.id }); + if (pinned) await unpinOthers(created.id); + await logActivity(`Information rédigée : ${title}`, "#6FB0C6"); + revalidateInformationPaths(); + return { id: created.id }; +} + +export async function publishInformation(formData: FormData) { + await requireInformations(); + const id = Number(formData.get("id") ?? 0); + if (!id) return; + + const [published] = await db + .update(informations) + .set({ status: "published", publishedAt: new Date(), updatedAt: new Date() }) + .where(and(eq(informations.id, id), eq(informations.status, "draft"))) + .returning({ id: informations.id, title: informations.title }); + if (!published) return; + + await logActivity(`Information publiée : ${published.title}`, "#1F8A5B"); + revalidateInformationPaths(); +} + +export async function unpublishInformation(formData: FormData) { + await requireInformations(); + const id = Number(formData.get("id") ?? 0); + if (!id) return; + + // Le retrait dépingle : une information invisible ne doit pas garder la + // place de tête au retour en ligne. + const [hidden] = await db + .update(informations) + .set({ status: "draft", pinned: false, updatedAt: new Date() }) + .where(eq(informations.id, id)) + .returning({ title: informations.title }); + if (!hidden) return; + + await logActivity(`Information retirée : ${hidden.title}`, "#9A6638"); + revalidateInformationPaths(); +} + +export async function toggleInformationPin(formData: FormData) { + await requireInformations(); + const id = Number(formData.get("id") ?? 0); + if (!id) return; + + const [current] = await db + .select({ pinned: informations.pinned }) + .from(informations) + .where(eq(informations.id, id)); + if (!current) return; + + const pinned = !current.pinned; + await db.update(informations).set({ pinned, updatedAt: new Date() }).where(eq(informations.id, id)); + if (pinned) await unpinOthers(id); + revalidateInformationPaths(); +} + +export async function deleteInformation(formData: FormData) { + await requireInformations(); + const id = Number(formData.get("id") ?? 0); + if (!id) return; + // Les lignes de lecture partent en cascade avec l'information. + await db.delete(informations).where(eq(informations.id, id)); + revalidateInformationPaths(); +} + +/** + * Marque des informations comme lues pour l'utilisateur courant. + * + * Appelée par un petit composant client après affichage, plutôt qu'au rendu de + * la page : l'écriture et le compteur de la barre latérale vivent dans deux + * composants serveur distincts, dont l'ordre de rendu n'est pas garanti. En + * passant par une action, les deux pastilles retombent ensemble. + */ +export async function markInformationsRead(ids: number[]) { + const session = await getSession(); + const userId = Number(session?.user.id); + if (!Number.isFinite(userId)) return; + + const clean = [...new Set(ids.map((id) => Number(id)).filter((id) => Number.isInteger(id) && id > 0))]; + if (clean.length === 0) return; + + await markRead(userId, clean); + revalidatePath("/backend/espace/informations"); + revalidatePath("/backend/espace"); + revalidatePath("/backend/espace/promotions"); + revalidatePath("/backend"); +} diff --git a/src/app/backend/espace/EspaceHeader.tsx b/src/app/backend/espace/EspaceHeader.tsx index 7f51e47..8a5fe98 100644 --- a/src/app/backend/espace/EspaceHeader.tsx +++ b/src/app/backend/espace/EspaceHeader.tsx @@ -5,6 +5,7 @@ function initialsOf(name: string) { } const TABS = [ + { key: "informations", href: "/backend/espace/informations", label: "Informations" }, { key: "profil", href: "/backend/espace", label: "Mon profil" }, { key: "promotions", href: "/backend/espace/promotions", label: "Mes promotions" }, ] as const; @@ -12,9 +13,12 @@ const TABS = [ export type EspaceTab = (typeof TABS)[number]["key"]; /** - * Bandeau de l'espace adhérent et onglets Profil / Promotions : deux pages - * distinctes pour que la fiche publique et la vie des promotions ne se - * mélangent pas dans une seule longue page. + * Bandeau de l'espace adhérent et ses onglets. Trois pages distinctes pour que + * les annonces de l'association, la fiche publique et la vie des promotions ne + * se mélangent pas dans une seule longue page. + * + * Les compteurs sont passés par chaque page : sans cela ils disparaîtraient + * en changeant d'onglet. */ export function EspaceHeader({ memberName, @@ -22,6 +26,7 @@ export function EspaceHeader({ active, publicPath, promoBadge, + infoBadge, }: { memberName: string; subtitle: string; @@ -29,6 +34,8 @@ export function EspaceHeader({ publicPath?: string | null; /** Nombre de promotions en ligne, affiché sur l'onglet. */ promoBadge?: number; + /** Nombre d'informations non lues. */ + infoBadge?: number; }) { return (
@@ -49,10 +56,11 @@ export function EspaceHeader({
)} - + {/* Résumé : ce qui est visible par le public aujourd'hui */}
diff --git a/src/app/backend/informations/InformationForm.tsx b/src/app/backend/informations/InformationForm.tsx new file mode 100644 index 0000000..88f9ab5 --- /dev/null +++ b/src/app/backend/informations/InformationForm.tsx @@ -0,0 +1,195 @@ +"use client"; + +import { useRef, useState } from "react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { ImageField } from "@/components/ImageField"; +import { richTextNodes } from "@/lib/rich-text"; +import { saveInformation } from "../actions"; + +/** + * Rédaction d'une information. + * + * Pas d'éditeur WYSIWYG : un `