diff --git a/drizzle/0005_round_exiles.sql b/drizzle/0005_round_exiles.sql new file mode 100644 index 0000000..6c10ac3 --- /dev/null +++ b/drizzle/0005_round_exiles.sql @@ -0,0 +1,11 @@ +DROP INDEX "meeting_registrations_meeting_member_idx";--> statement-breakpoint +ALTER TABLE "meeting_registrations" ADD COLUMN "attendee_company" varchar(200) DEFAULT '' NOT NULL;--> statement-breakpoint +ALTER TABLE "meeting_registrations" ADD COLUMN "attendee_phone" varchar(40);--> statement-breakpoint +ALTER TABLE "meeting_registrations" ADD COLUMN "status" varchar(20) DEFAULT 'pending' NOT NULL;--> statement-breakpoint +UPDATE "meeting_registrations" AS r +SET "attendee_company" = m."name", "attendee_phone" = m."phone", "status" = 'confirmed' +FROM "members" AS m +WHERE r."member_id" = m."id";--> statement-breakpoint +ALTER TABLE "meetings" ADD COLUMN "participants_per_account" integer DEFAULT 1 NOT NULL;--> statement-breakpoint +CREATE INDEX "meeting_registrations_meeting_idx" ON "meeting_registrations" USING btree ("meeting_id");--> statement-breakpoint +CREATE INDEX "meeting_registrations_member_meeting_idx" ON "meeting_registrations" USING btree ("member_id","meeting_id"); diff --git a/drizzle/meta/0005_snapshot.json b/drizzle/meta/0005_snapshot.json new file mode 100644 index 0000000..dbf9b6f --- /dev/null +++ b/drizzle/meta/0005_snapshot.json @@ -0,0 +1,1077 @@ +{ + "id": "9b4bca4a-67df-47b5-83c4-59046495e751", + "prevId": "8ae94381-2b0a-4691-b3a4-b59ff6ff8542", + "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.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 + }, + "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'" + }, + "valid_until": { + "name": "valid_until", + "type": "varchar(120)", + "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" + } + }, + "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.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 + }, + "temp_password": { + "name": "temp_password", + "type": "varchar(60)", + "primaryKey": false, + "notNull": false + }, + "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.member_status": { + "name": "member_status", + "schema": "public", + "values": [ + "active", + "pending" + ] + }, + "public.promo_status": { + "name": "promo_status", + "schema": "public", + "values": [ + "pending", + "live", + "expired", + "rejected" + ] + }, + "public.request_status": { + "name": "request_status", + "schema": "public", + "values": [ + "new", + "approved", + "rejected" + ] + }, + "public.role": { + "name": "role", + "schema": "public", + "values": [ + "admin", + "moderator", + "editor", + "member" + ] + } + }, + "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 d78f4ac..2ed7732 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -36,6 +36,13 @@ "when": 1784303079565, "tag": "0004_tiny_vanisher", "breakpoints": true + }, + { + "idx": 5, + "version": "7", + "when": 1784339279276, + "tag": "0005_round_exiles", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/app/association/page.tsx b/src/app/association/page.tsx index ae617bd..0fa2142 100644 --- a/src/app/association/page.tsx +++ b/src/app/association/page.tsx @@ -1,18 +1,11 @@ import Link from "next/link"; -import { and, asc, desc, eq, gte, inArray, sql } from "drizzle-orm"; +import { and, asc, eq, gte, inArray, sql } from "drizzle-orm"; import { auth } from "@/auth"; import { db } from "@/db"; -import { - meetingRegistrations, - meetings, - pastMeetingPhotos, - pastMeetings, -} from "@/db/schema"; -import { PastMeetingGallery } from "@/components/PastMeetingGallery"; +import { meetingRegistrations, meetings } from "@/db/schema"; import { SiteFooter } from "@/components/SiteFooter"; import { SiteHeader } from "@/components/SiteHeader"; -import { getSiteSettings, parseBoardMembers, splitLines } from "@/lib/site-settings"; -import { cancelMeetingRegistration, registerForMeeting } from "../backend/actions"; +import { getSiteSettings, parseBoardMembers } from "@/lib/site-settings"; export const dynamic = "force-dynamic"; @@ -42,6 +35,7 @@ export default async function AssociationPage() { location: meetings.location, description: meetings.description, capacity: meetings.capacity, + participantsPerAccount: meetings.participantsPerAccount, imageUrl: meetings.imageUrl, registered: sql`count(${meetingRegistrations.id})`, }) @@ -51,29 +45,6 @@ export default async function AssociationPage() { .groupBy(meetings.id) .orderBy(asc(meetings.startsAt)); - const past = await db - .select() - .from(pastMeetings) - .orderBy(desc(pastMeetings.eventDate)); - - const photoRows = await db - .select() - .from(pastMeetingPhotos) - .orderBy(asc(pastMeetingPhotos.position), asc(pastMeetingPhotos.id)); - - const meetingIds = past.map((item) => item.meetingId).filter((id): id is number => id != null); - const linkedRegistrations = - meetingIds.length > 0 - ? await db - .select({ - meetingId: meetingRegistrations.meetingId, - attendeeName: meetingRegistrations.attendeeName, - }) - .from(meetingRegistrations) - .where(inArray(meetingRegistrations.meetingId, meetingIds)) - .orderBy(asc(meetingRegistrations.attendeeName)) - : []; - const myRegistrations = memberId && upcoming.length > 0 ? await db @@ -91,18 +62,6 @@ export default async function AssociationPage() { : []; const myMeetingIds = new Set(myRegistrations.map((row) => row.meetingId)); - const photosByPast = new Map(); - for (const photo of photoRows) { - photosByPast.set(photo.pastMeetingId, [...(photosByPast.get(photo.pastMeetingId) ?? []), photo]); - } - const registrationsByMeeting = new Map(); - for (const reg of linkedRegistrations) { - registrationsByMeeting.set(reg.meetingId, [ - ...(registrationsByMeeting.get(reg.meetingId) ?? []), - reg.attendeeName, - ]); - } - const officers = [ ["Vice-présidence", settings.association_vice_president], ["Trésorerie", settings.association_treasurer], @@ -191,31 +150,15 @@ export default async function AssociationPage() { {registered}/{meeting.capacity} inscrits {remaining} place(s) - {memberId ? ( - isRegistered ? ( -
- - -
- ) : ( -
- - - -
- ) - ) : ( - - Connexion adhérent - - )} +
+ Jusqu'à {meeting.participantsPerAccount} participant{meeting.participantsPerAccount > 1 ? "s" : ""} par compte +
+ + {isRegistered ? "Modifier mon inscription" : remaining <= 0 ? "Complet" : memberId ? "S'inscrire" : "Connexion et inscription"} + ); @@ -223,48 +166,6 @@ export default async function AssociationPage() { -
-
-
- Archives -
-

- Rencontres passées -

- {past.length === 0 && ( -
- Aucune rencontre passée publiée pour le moment. -
- )} -
- {past.map((item) => { - const linkedNames = item.meetingId ? registrationsByMeeting.get(item.meetingId) ?? [] : []; - const manualNames = splitLines(item.participants ?? ""); - const names = linkedNames.length > 0 ? linkedNames : manualNames; - return ( -
-
- {formatDate(item.eventDate)} -
-

- {item.title} -

- {item.location &&
{item.location}
} -

{item.description}

- {names.length > 0 && ( -
- Participants: {names.slice(0, 6).join(", ")} - {names.length > 6 ? ` +${names.length - 6}` : ""} -
- )} - -
- ); - })} -
-
-
-
Gouvernance diff --git a/src/app/backend/BackendShell.tsx b/src/app/backend/BackendShell.tsx index aa94198..2d18712 100644 --- a/src/app/backend/BackendShell.tsx +++ b/src/app/backend/BackendShell.tsx @@ -15,7 +15,9 @@ const TITLES: Record = { "Modération des promotions", "Validez les offres soumises par les adhérents", ], - "/backend/rencontres": ["Rencontres", "Gérer les prochaines dates et les archives"], + "/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/emails": ["Création d'e-mails", "Composer des messages aux couleurs de Plein R"], "/backend/administrateurs": ["Administrateurs", "Gérer les accès à l'administration"], "/backend/categories": ["Catégories", "Gérer les métiers de l'annuaire"], @@ -149,6 +151,16 @@ export function BackendShell({ {dotRound}Rencontres )} + {can(user.role, "manageMeetings") && ( + + {dot}Inscriptions + + )} + {can(user.role, "manageMeetings") && ( + + {dotDiamond}Rencontres passées + + )} {can(user.role, "manageEmails") && ( {dot}E-mails diff --git a/src/app/backend/actions.ts b/src/app/backend/actions.ts index 64beae9..c165934 100644 --- a/src/app/backend/actions.ts +++ b/src/app/backend/actions.ts @@ -406,11 +406,13 @@ export async function createMeeting(formData: FormData) { startsAt: asDate(formData, "startsAt"), location: asNullableString(formData, "location"), description: asNullableString(formData, "description"), - capacity: Number(formData.get("capacity") ?? 30) || 30, + capacity: Math.min(100000, Math.max(1, Number(formData.get("capacity") ?? 30) || 30)), + participantsPerAccount: Math.min(100, Math.max(1, Number(formData.get("participantsPerAccount") ?? 1) || 1)), imageUrl: asNullableString(formData, "imageUrl"), }); await logActivity(`Rencontre ajoutée : ${title}`, "#2C6FB3"); revalidatePath("/backend/rencontres"); + revalidatePath("/backend/inscriptions"); revalidatePath("/association"); } @@ -428,11 +430,13 @@ export async function updateMeeting(formData: FormData) { startsAt: asDate(formData, "startsAt"), location: asNullableString(formData, "location"), description: asNullableString(formData, "description"), - capacity: Number(formData.get("capacity") ?? 30) || 30, + capacity: Math.min(100000, Math.max(1, Number(formData.get("capacity") ?? 30) || 30)), + participantsPerAccount: Math.min(100, Math.max(1, Number(formData.get("participantsPerAccount") ?? 1) || 1)), imageUrl: asNullableString(formData, "imageUrl"), }) .where(eq(meetings.id, id)); revalidatePath("/backend/rencontres"); + revalidatePath("/backend/inscriptions"); revalidatePath("/association"); } @@ -443,65 +447,170 @@ export async function deleteMeeting(formData: FormData) { if (!id) return; await db.delete(meetings).where(eq(meetings.id, id)); revalidatePath("/backend/rencontres"); + revalidatePath("/backend/inscriptions"); revalidatePath("/association"); } -export async function registerForMeeting(formData: FormData) { +export type MeetingRegistrationState = { + status: "idle" | "success" | "error"; + message: string; + registrationCount?: number; +}; + +export async function saveMeetingRegistration( + _previousState: MeetingRegistrationState, + formData: FormData, +): Promise { const session = await auth(); - if (!session?.user?.memberId) throw new Error("Compte adhérent requis"); + if (!session?.user?.memberId) { + return { status: "error", message: "Connectez-vous avec un compte adhérent pour vous inscrire." }; + } const memberId = session.user.memberId; const meetingId = Number(formData.get("meetingId")); - if (!meetingId) return; + if (!meetingId) return { status: "error", message: "Rencontre introuvable." }; - await db.transaction(async (tx) => { - const [meeting] = await tx.select().from(meetings).where(eq(meetings.id, meetingId)); - if (!meeting) throw new Error("Rencontre introuvable"); - if (meeting.startsAt.getTime() < Date.now()) throw new Error("Cette rencontre est passée"); + try { + if (asString(formData, "intent") === "cancel") { + await db + .delete(meetingRegistrations) + .where(and(eq(meetingRegistrations.meetingId, meetingId), eq(meetingRegistrations.memberId, memberId))); + revalidateMeetingPaths(meetingId); + return { status: "success", message: "Votre inscription a été annulée.", registrationCount: 0 }; + } - const duplicate = await tx - .select({ id: meetingRegistrations.id }) - .from(meetingRegistrations) - .where(and(eq(meetingRegistrations.meetingId, meetingId), eq(meetingRegistrations.memberId, memberId))); - if (duplicate.length > 0) return; + const participantCount = Number(formData.get("participantCount")); + if (!Number.isInteger(participantCount) || participantCount < 1 || participantCount > 100) { + return { status: "error", message: "Ajoutez au moins un participant." }; + } - const [{ total } = { total: 0 }] = await tx - .select({ total: sql`count(*)` }) - .from(meetingRegistrations) - .where(eq(meetingRegistrations.meetingId, meetingId)); - if (Number(total) >= meeting.capacity) throw new Error("Rencontre complète"); + const participants = Array.from({ length: participantCount }, (_, index) => ({ + name: asString(formData, `participantName-${index}`), + imageConsent: formData.get(`imageConsent-${index}`) === "on", + })); + if (participants.some((participant) => !participant.name || participant.name.length > 200)) { + return { status: "error", message: "Renseignez le nom et le prénom de chaque participant." }; + } + const normalizedNames = participants.map((participant) => participant.name.toLocaleLowerCase("fr-FR")); + if (new Set(normalizedNames).size !== normalizedNames.length) { + return { status: "error", message: "Chaque participant doit avoir un nom différent." }; + } + if (formData.get("attestation") !== "on") { + return { status: "error", message: "Vous devez confirmer avoir recueilli le choix de chaque participant." }; + } - const [member] = await tx - .select({ name: members.name, email: members.email }) - .from(members) - .where(eq(members.id, memberId)); - await tx.insert(meetingRegistrations).values({ - meetingId, - memberId, - attendeeName: member?.name ?? session.user.name ?? "Adhérent", - attendeeEmail: member?.email ?? session.user.email ?? null, - imageConsent: formData.get("imageConsent") === "on", + await db.transaction(async (tx) => { + await tx.execute(sql`select ${meetings.id} from ${meetings} where ${meetings.id} = ${meetingId} for update`); + const [meeting] = await tx.select().from(meetings).where(eq(meetings.id, meetingId)); + if (!meeting) throw new Error("Rencontre introuvable."); + if (meeting.startsAt.getTime() < Date.now()) throw new Error("Cette rencontre est passée."); + if (participants.length > meeting.participantsPerAccount) { + throw new Error(`Cette rencontre autorise ${meeting.participantsPerAccount} participant(s) maximum par compte.`); + } + + const [member] = await tx + .select({ name: members.name, email: members.email, phone: members.phone, status: members.status }) + .from(members) + .where(eq(members.id, memberId)); + if (!member || member.status !== "active") { + throw new Error("Votre adhésion doit être active pour vous inscrire."); + } + + const [{ total } = { total: 0 }] = await tx + .select({ total: sql`count(*)` }) + .from(meetingRegistrations) + .where( + and( + eq(meetingRegistrations.meetingId, meetingId), + sql`${meetingRegistrations.memberId} is distinct from ${memberId}`, + ), + ); + if (Number(total) + participants.length > meeting.capacity) { + throw new Error("Il ne reste pas assez de places pour tous les participants."); + } + + await tx + .delete(meetingRegistrations) + .where(and(eq(meetingRegistrations.meetingId, meetingId), eq(meetingRegistrations.memberId, memberId))); + await tx.insert(meetingRegistrations).values( + participants.map((participant) => ({ + meetingId, + memberId, + attendeeName: participant.name, + attendeeCompany: member.name, + attendeeEmail: member.email ?? session.user.email ?? null, + attendeePhone: member.phone, + status: "pending", + imageConsent: participant.imageConsent, + })), + ); }); - }); - revalidatePath("/association"); - revalidatePath("/backend/espace"); + revalidateMeetingPaths(meetingId); + return { + status: "success", + message: `Inscription enregistrée pour ${participants.length} participant${participants.length > 1 ? "s" : ""}.`, + registrationCount: participants.length, + }; + } catch (error) { + return { status: "error", message: error instanceof Error ? error.message : "Inscription impossible." }; + } } -export async function cancelMeetingRegistration(formData: FormData) { - const session = await auth(); - if (!session?.user?.memberId) throw new Error("Compte adhérent requis"); - const meetingId = Number(formData.get("meetingId")); - if (!meetingId) return; - await db - .delete(meetingRegistrations) - .where( - and( - eq(meetingRegistrations.meetingId, meetingId), - eq(meetingRegistrations.memberId, session.user.memberId) - ) - ); +function revalidateMeetingPaths(meetingId?: number) { revalidatePath("/association"); + revalidatePath("/backend/rencontres"); + revalidatePath("/backend/inscriptions"); + revalidatePath("/backend/rencontres-passees"); revalidatePath("/backend/espace"); + revalidatePath("/rencontres-passees"); + if (meetingId) revalidatePath(`/inscription/${meetingId}`); +} + +export async function updateMeetingRegistration(formData: FormData) { + const { role } = await requireRole(); + if (!can(role, "manageMeetings")) throw new Error("Accès refusé"); + const id = Number(formData.get("id")); + const attendeeName = asString(formData, "attendeeName"); + const status = asString(formData, "status"); + if (!id || !attendeeName || !["pending", "confirmed"].includes(status)) return; + const [registration] = await db + .update(meetingRegistrations) + .set({ + attendeeName, + attendeeCompany: asString(formData, "attendeeCompany"), + attendeeEmail: asNullableString(formData, "attendeeEmail"), + attendeePhone: asNullableString(formData, "attendeePhone"), + status, + imageConsent: formData.get("imageConsent") === "on", + }) + .where(eq(meetingRegistrations.id, id)) + .returning({ meetingId: meetingRegistrations.meetingId }); + revalidateMeetingPaths(registration?.meetingId); +} + +export async function confirmMeetingRegistration(formData: FormData) { + const { role } = await requireRole(); + if (!can(role, "manageMeetings")) throw new Error("Accès refusé"); + const id = Number(formData.get("id")); + if (!id) return; + const [registration] = await db + .update(meetingRegistrations) + .set({ status: "confirmed" }) + .where(eq(meetingRegistrations.id, id)) + .returning({ meetingId: meetingRegistrations.meetingId }); + revalidateMeetingPaths(registration?.meetingId); +} + +export async function deleteMeetingRegistration(formData: FormData) { + const { role } = await requireRole(); + if (!can(role, "manageMeetings")) throw new Error("Accès refusé"); + const id = Number(formData.get("id")); + if (!id) return; + const [registration] = await db + .delete(meetingRegistrations) + .where(eq(meetingRegistrations.id, id)) + .returning({ meetingId: meetingRegistrations.meetingId }); + revalidateMeetingPaths(registration?.meetingId); } export async function createPastMeeting(formData: FormData) { @@ -519,6 +628,8 @@ export async function createPastMeeting(formData: FormData) { }); revalidatePath("/backend/rencontres"); revalidatePath("/association"); + revalidatePath("/backend/rencontres-passees"); + revalidatePath("/rencontres-passees"); } export async function updatePastMeeting(formData: FormData) { @@ -540,6 +651,8 @@ export async function updatePastMeeting(formData: FormData) { .where(eq(pastMeetings.id, id)); revalidatePath("/backend/rencontres"); revalidatePath("/association"); + revalidatePath("/backend/rencontres-passees"); + revalidatePath("/rencontres-passees"); } export async function deletePastMeeting(formData: FormData) { @@ -550,6 +663,8 @@ export async function deletePastMeeting(formData: FormData) { await db.delete(pastMeetings).where(eq(pastMeetings.id, id)); revalidatePath("/backend/rencontres"); revalidatePath("/association"); + revalidatePath("/backend/rencontres-passees"); + revalidatePath("/rencontres-passees"); } export async function addPastMeetingPhoto(formData: FormData) { @@ -570,6 +685,8 @@ export async function addPastMeetingPhoto(formData: FormData) { }); revalidatePath("/backend/rencontres"); revalidatePath("/association"); + revalidatePath("/backend/rencontres-passees"); + revalidatePath("/rencontres-passees"); } export async function deletePastMeetingPhoto(formData: FormData) { @@ -580,6 +697,8 @@ export async function deletePastMeetingPhoto(formData: FormData) { await db.delete(pastMeetingPhotos).where(eq(pastMeetingPhotos.id, id)); revalidatePath("/backend/rencontres"); revalidatePath("/association"); + revalidatePath("/backend/rencontres-passees"); + revalidatePath("/rencontres-passees"); } // ---- Paramètres du site ---- diff --git a/src/app/backend/espace/page.tsx b/src/app/backend/espace/page.tsx index 90b1eea..d15554b 100644 --- a/src/app/backend/espace/page.tsx +++ b/src/app/backend/espace/page.tsx @@ -1,7 +1,8 @@ -import { asc, desc, eq } from "drizzle-orm"; +import Link from "next/link"; +import { and, asc, desc, eq, gte, sql } from "drizzle-orm"; import { auth } from "@/auth"; import { db } from "@/db"; -import { categories, imageConsents, members, promotions, type Member } from "@/db/schema"; +import { categories, imageConsents, meetingRegistrations, meetings, members, promotions, type Member } from "@/db/schema"; import { ImageField } from "@/components/ImageField"; import { ImageConsentForm } from "@/components/ImageConsentForm"; import { communeOptions } from "@/lib/communes"; @@ -30,6 +31,7 @@ export default async function EspacePage() { let subtitle = "Espace adhérent"; let profile: Member | null = null; let myPromos: { id: number; title: string; status: string; createdAt: Date; imageUrl: string | null }[] = []; + let myRegistrations: { meetingId: number; title: string; startsAt: Date; location: string | null; participants: number; confirmed: boolean }[] = []; let latestConsent: { decision: string; createdAt: Date } | null = null; if (memberId) { @@ -53,6 +55,20 @@ export default async function EspacePage() { .from(promotions) .where(eq(promotions.memberId, memberId)) .orderBy(desc(promotions.createdAt)); + myRegistrations = await db + .select({ + meetingId: meetings.id, + title: meetings.title, + startsAt: meetings.startsAt, + location: meetings.location, + participants: sql`count(${meetingRegistrations.id})`, + confirmed: sql`bool_and(${meetingRegistrations.status} = 'confirmed')`, + }) + .from(meetingRegistrations) + .innerJoin(meetings, eq(meetings.id, meetingRegistrations.meetingId)) + .where(and(eq(meetingRegistrations.memberId, memberId), gte(meetings.startsAt, new Date()))) + .groupBy(meetings.id) + .orderBy(asc(meetings.startsAt)); const [consent] = await db .select({ decision: imageConsents.decision, createdAt: imageConsents.createdAt }) .from(imageConsents) @@ -93,6 +109,37 @@ export default async function EspacePage() {
+ {memberId && ( +
+
+
+

Mes inscriptions

+
Participants inscrits aux prochaines rencontres.
+
+ Voir les rencontres +
+ {myRegistrations.length === 0 &&
Aucune inscription à venir.
} +
+ {myRegistrations.map((registration) => ( +
+
+
{registration.title}
+
+ {fmtDate(registration.startsAt)} · {registration.participants} participant{registration.participants > 1 ? "s" : ""}{registration.location ? ` · ${registration.location}` : ""} +
+
+
+ + {registration.confirmed ? "Confirmée" : "En attente"} + + Modifier +
+
+ ))} +
+
+ )} + {profile && (

diff --git a/src/app/backend/inscriptions/page.tsx b/src/app/backend/inscriptions/page.tsx new file mode 100644 index 0000000..9793b10 --- /dev/null +++ b/src/app/backend/inscriptions/page.tsx @@ -0,0 +1,143 @@ +import { redirect } from "next/navigation"; +import type { CSSProperties } from "react"; +import { and, desc, eq } from "drizzle-orm"; +import { auth } from "@/auth"; +import { db } from "@/db"; +import { meetingRegistrations, meetings } from "@/db/schema"; +import { can } from "@/lib/rbac"; +import { + confirmMeetingRegistration, + deleteMeetingRegistration, + updateMeetingRegistration, +} from "../actions"; + +export const dynamic = "force-dynamic"; + +function formatDate(date: Date) { + return new Intl.DateTimeFormat("fr-FR", { day: "numeric", month: "short", year: "numeric", hour: "2-digit", minute: "2-digit" }).format(date); +} + +export default async function InscriptionsPage({ searchParams }: { searchParams: Promise<{ meeting?: string }> }) { + const session = await auth(); + if (!can(session?.user.role, "manageMeetings")) redirect("/backend"); + const { meeting: meetingParam } = await searchParams; + const selectedMeetingId = Number(meetingParam) || null; + + const [meetingRows, registrationRows] = await Promise.all([ + db.select({ id: meetings.id, title: meetings.title, startsAt: meetings.startsAt }).from(meetings).orderBy(desc(meetings.startsAt)), + db + .select({ + id: meetingRegistrations.id, + meetingId: meetingRegistrations.meetingId, + meetingTitle: meetings.title, + meetingStartsAt: meetings.startsAt, + attendeeName: meetingRegistrations.attendeeName, + attendeeCompany: meetingRegistrations.attendeeCompany, + attendeeEmail: meetingRegistrations.attendeeEmail, + attendeePhone: meetingRegistrations.attendeePhone, + status: meetingRegistrations.status, + imageConsent: meetingRegistrations.imageConsent, + createdAt: meetingRegistrations.createdAt, + }) + .from(meetingRegistrations) + .innerJoin(meetings, eq(meetings.id, meetingRegistrations.meetingId)) + .where(selectedMeetingId ? and(eq(meetingRegistrations.meetingId, selectedMeetingId)) : undefined) + .orderBy(desc(meetings.startsAt), desc(meetingRegistrations.createdAt)), + ]); + + return ( +
+
+
+
Participants
+
{registrationRows.length} inscription{registrationRows.length > 1 ? "s" : ""} affichée{registrationRows.length > 1 ? "s" : ""}
+
+ +
+ + +
+ + +
+ + {registrationRows.map((registration) => ( +
+
+
+
{registration.attendeeName}
+
{registration.meetingTitle} · {formatDate(registration.meetingStartsAt)}
+
+
+ {registration.status === "confirmed" ? "Confirmée" : "En attente"} + {registration.imageConsent ? "Image autorisée" : "Image refusée"} +
+
+ +
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+ +
+ {registration.status !== "confirmed" && ( +
+ + +
+ )} +
+ + +
+
+
+ ))} + + {registrationRows.length === 0 &&
Aucune inscription pour cette sélection.
} +
+ ); +} + +const sectionLabel: CSSProperties = { fontSize: 12.5, letterSpacing: "0.08em", textTransform: "uppercase", color: "#9a8d72", fontWeight: 800 }; +const cardStyle: CSSProperties = { background: "#fff", border: "1px solid #e6dcc6", borderRadius: 8, padding: 20 }; +const badgeBase: CSSProperties = { display: "inline-block", borderRadius: 999, padding: "5px 10px", fontSize: 11.5, fontWeight: 800 }; +const confirmedBadge: CSSProperties = { ...badgeBase, background: "#e6f4ec", color: "#1f8a5b" }; +const pendingBadge: CSSProperties = { ...badgeBase, background: "#fbeede", color: "#9a6638" }; +const consentBadge: CSSProperties = { ...badgeBase, background: "#eaf0f6", color: "#2C6FB3" }; +const refusalBadge: CSSProperties = { ...badgeBase, background: "#fbe9e6", color: "#d8472b" }; +const primaryButton: CSSProperties = { border: "none", background: "#13324F", color: "#fff", borderRadius: 8, padding: "11px 16px", fontWeight: 800, cursor: "pointer" }; +const secondaryButton: CSSProperties = { border: "1px solid #d8cdb4", background: "#fff", color: "#6f6450", borderRadius: 8, padding: "11px 16px", fontWeight: 800, cursor: "pointer" }; +const confirmButton: CSSProperties = { border: "none", background: "#1f8a5b", color: "#fff", borderRadius: 8, padding: "9px 14px", fontWeight: 800, cursor: "pointer" }; +const dangerButton: CSSProperties = { border: "1px solid #e0c3bb", background: "#fff", color: "#d8472b", borderRadius: 8, padding: "8px 13px", fontWeight: 800, cursor: "pointer" }; +const emptyStyle: CSSProperties = { background: "#fff", border: "1px solid #e6dcc6", borderRadius: 8, padding: 28, color: "#8c8068", textAlign: "center" }; diff --git a/src/app/backend/rencontres-passees/page.tsx b/src/app/backend/rencontres-passees/page.tsx new file mode 100644 index 0000000..073405d --- /dev/null +++ b/src/app/backend/rencontres-passees/page.tsx @@ -0,0 +1,172 @@ +import { redirect } from "next/navigation"; +import type { CSSProperties } from "react"; +import { asc, desc, eq, sql } from "drizzle-orm"; +import { auth } from "@/auth"; +import { db } from "@/db"; +import { meetingRegistrations, meetings, pastMeetingPhotos, pastMeetings } from "@/db/schema"; +import { ImageField } from "@/components/ImageField"; +import { can } from "@/lib/rbac"; +import { + addPastMeetingPhoto, + createPastMeeting, + deletePastMeeting, + deletePastMeetingPhoto, + updatePastMeeting, +} from "../actions"; + +export const dynamic = "force-dynamic"; + +function dateValue(date: Date) { + const local = new Date(date); + local.setMinutes(local.getMinutes() - local.getTimezoneOffset()); + return local.toISOString().slice(0, 10); +} + +export default async function PastMeetingsAdminPage() { + const session = await auth(); + if (!can(session?.user.role, "manageMeetings")) redirect("/backend"); + + const [meetingRows, archives, photos, refusals] = await Promise.all([ + db + .select({ + id: meetings.id, + title: meetings.title, + startsAt: meetings.startsAt, + registered: sql`count(${meetingRegistrations.id})`, + }) + .from(meetings) + .leftJoin(meetingRegistrations, eq(meetingRegistrations.meetingId, meetings.id)) + .groupBy(meetings.id) + .orderBy(desc(meetings.startsAt)), + db.select().from(pastMeetings).orderBy(desc(pastMeetings.eventDate)), + db.select().from(pastMeetingPhotos).orderBy(asc(pastMeetingPhotos.position), asc(pastMeetingPhotos.id)), + db + .select({ meetingId: meetingRegistrations.meetingId, attendeeName: meetingRegistrations.attendeeName }) + .from(meetingRegistrations) + .where(eq(meetingRegistrations.imageConsent, false)) + .orderBy(asc(meetingRegistrations.attendeeName)), + ]); + + const photosByPast = new Map(); + for (const photo of photos) photosByPast.set(photo.pastMeetingId, [...(photosByPast.get(photo.pastMeetingId) ?? []), photo]); + const refusalsByMeeting = new Map(); + for (const refusal of refusals) refusalsByMeeting.set(refusal.meetingId, [...(refusalsByMeeting.get(refusal.meetingId) ?? []), refusal.attendeeName]); + + return ( +
+
+

Nouvelle rencontre passée

+

Les archives publiées apparaissent sur la page publique « Rencontres passées ».

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +