Bunch of updates to scheduling

This commit is contained in:
2026-03-15 13:50:09 -06:00
parent 12734aca16
commit 28e4bc9475
202 changed files with 4568 additions and 226 deletions

View File

@@ -0,0 +1,12 @@
-- Add POLL to CalendarItemSource enum
ALTER TYPE "CalendarItemSource" ADD VALUE IF NOT EXISTS 'POLL';
-- AlterTable
ALTER TABLE "scheduling_polls" ADD COLUMN "auto_finalize" BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE "scheduling_polls" ADD COLUMN "auto_finalize_threshold" INTEGER;
ALTER TABLE "scheduling_polls" ADD COLUMN "auto_convert_to_calendar" BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE "scheduling_polls" ADD COLUMN "auto_convert_to_gancio" BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE "scheduling_polls" ADD COLUMN "auto_convert_to_shift" BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE "scheduling_polls" ADD COLUMN "tie_breaker" TEXT NOT NULL DEFAULT 'earliest';
ALTER TABLE "scheduling_polls" ADD COLUMN "auto_finalize_job_id" TEXT;
ALTER TABLE "scheduling_polls" ADD COLUMN "converted_calendar_item_id" TEXT;

View File

@@ -0,0 +1,160 @@
-- AlterEnum
ALTER TYPE "ContactActivityType" ADD VALUE 'POLL_VOTED';
-- AlterEnum
ALTER TYPE "ContactSource" ADD VALUE 'POLL_VOTE';
-- AlterEnum
ALTER TYPE "SignupSource" ADD VALUE 'POLL_CONVERSION';
-- AlterTable
ALTER TABLE "contacts" ADD COLUMN "pronouns" TEXT;
-- AlterTable
ALTER TABLE "scheduling_poll_votes" ADD COLUMN "contact_id" TEXT;
-- AlterTable
ALTER TABLE "scheduling_polls" ADD COLUMN "auto_enroll_voters" BOOLEAN NOT NULL DEFAULT true;
-- AlterTable
ALTER TABLE "users" ADD COLUMN "pronouns" TEXT;
-- CreateTable
CREATE TABLE "participant_needs" (
"id" TEXT NOT NULL,
"user_id" TEXT,
"contact_id" TEXT,
"needs_wheelchair" BOOLEAN NOT NULL DEFAULT false,
"needs_ground_floor" BOOLEAN NOT NULL DEFAULT false,
"needs_hearing_loop" BOOLEAN NOT NULL DEFAULT false,
"needs_sign_language" BOOLEAN NOT NULL DEFAULT false,
"other_accessibility" TEXT,
"is_vegan" BOOLEAN NOT NULL DEFAULT false,
"is_vegetarian" BOOLEAN NOT NULL DEFAULT false,
"is_gluten_free" BOOLEAN NOT NULL DEFAULT false,
"is_halal" BOOLEAN NOT NULL DEFAULT false,
"is_kosher" BOOLEAN NOT NULL DEFAULT false,
"has_nut_allergy" BOOLEAN NOT NULL DEFAULT false,
"other_dietary" TEXT,
"needs_childcare" BOOLEAN NOT NULL DEFAULT false,
"childcare_details" TEXT,
"needs_transportation" BOOLEAN NOT NULL DEFAULT false,
"transportation_notes" TEXT,
"preferred_language" TEXT DEFAULT 'en',
"needs_translation" BOOLEAN NOT NULL DEFAULT false,
"translation_language" TEXT,
"visibility_consent" TEXT NOT NULL DEFAULT 'organizer_only',
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "participant_needs_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "meeting_agendas" (
"id" TEXT NOT NULL,
"shift_id" TEXT,
"poll_id" TEXT,
"title" TEXT NOT NULL,
"items" JSONB NOT NULL DEFAULT '[]',
"status" TEXT NOT NULL DEFAULT 'draft',
"created_by_user_id" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "meeting_agendas_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "meeting_minutes" (
"id" TEXT NOT NULL,
"agenda_id" TEXT NOT NULL,
"notes" TEXT NOT NULL,
"decisions" JSONB NOT NULL DEFAULT '[]',
"attendees" JSONB NOT NULL DEFAULT '[]',
"approved_at" TIMESTAMP(3),
"approved_by_user_id" TEXT,
"created_by_user_id" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "meeting_minutes_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "action_items" (
"id" TEXT NOT NULL,
"agenda_id" TEXT,
"title" TEXT NOT NULL,
"description" TEXT,
"assignee_user_id" TEXT,
"due_date" TIMESTAMP(3),
"status" TEXT NOT NULL DEFAULT 'open',
"priority" TEXT NOT NULL DEFAULT 'normal',
"completed_at" TIMESTAMP(3),
"created_by_user_id" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "action_items_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "participant_needs_user_id_key" ON "participant_needs"("user_id");
-- CreateIndex
CREATE UNIQUE INDEX "participant_needs_contact_id_key" ON "participant_needs"("contact_id");
-- CreateIndex
CREATE UNIQUE INDEX "meeting_agendas_shift_id_key" ON "meeting_agendas"("shift_id");
-- CreateIndex
CREATE UNIQUE INDEX "meeting_agendas_poll_id_key" ON "meeting_agendas"("poll_id");
-- CreateIndex
CREATE UNIQUE INDEX "meeting_minutes_agenda_id_key" ON "meeting_minutes"("agenda_id");
-- CreateIndex
CREATE INDEX "action_items_assignee_user_id_status_idx" ON "action_items"("assignee_user_id", "status");
-- CreateIndex
CREATE INDEX "action_items_due_date_idx" ON "action_items"("due_date");
-- CreateIndex
CREATE INDEX "scheduling_poll_votes_contact_id_idx" ON "scheduling_poll_votes"("contact_id");
-- AddForeignKey
ALTER TABLE "scheduling_poll_votes" ADD CONSTRAINT "scheduling_poll_votes_contact_id_fkey" FOREIGN KEY ("contact_id") REFERENCES "contacts"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "participant_needs" ADD CONSTRAINT "participant_needs_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "participant_needs" ADD CONSTRAINT "participant_needs_contact_id_fkey" FOREIGN KEY ("contact_id") REFERENCES "contacts"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "meeting_agendas" ADD CONSTRAINT "meeting_agendas_shift_id_fkey" FOREIGN KEY ("shift_id") REFERENCES "shifts"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "meeting_agendas" ADD CONSTRAINT "meeting_agendas_poll_id_fkey" FOREIGN KEY ("poll_id") REFERENCES "scheduling_polls"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "meeting_agendas" ADD CONSTRAINT "meeting_agendas_created_by_user_id_fkey" FOREIGN KEY ("created_by_user_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "meeting_minutes" ADD CONSTRAINT "meeting_minutes_agenda_id_fkey" FOREIGN KEY ("agenda_id") REFERENCES "meeting_agendas"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "meeting_minutes" ADD CONSTRAINT "meeting_minutes_approved_by_user_id_fkey" FOREIGN KEY ("approved_by_user_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "meeting_minutes" ADD CONSTRAINT "meeting_minutes_created_by_user_id_fkey" FOREIGN KEY ("created_by_user_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "action_items" ADD CONSTRAINT "action_items_agenda_id_fkey" FOREIGN KEY ("agenda_id") REFERENCES "meeting_agendas"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "action_items" ADD CONSTRAINT "action_items_assignee_user_id_fkey" FOREIGN KEY ("assignee_user_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "action_items" ADD CONSTRAINT "action_items_created_by_user_id_fkey" FOREIGN KEY ("created_by_user_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -48,6 +48,7 @@ model User {
password String // bcrypt hashed
name String?
phone String?
pronouns String?
role UserRole @default(USER)
roles Json @default("[]") // Array of UserRole strings for multi-role support
status UserStatus @default(ACTIVE)
@@ -166,6 +167,16 @@ model User {
schedulingPollVotes SchedulingPollVote[] @relation("PollVoter")
schedulingPollComments SchedulingPollComment[] @relation("PollCommenter")
// Participant needs
participantNeeds ParticipantNeeds? @relation("UserParticipantNeeds")
// Meeting agendas & action items
agendasCreated MeetingAgenda[] @relation("AgendaCreator")
minutesCreated MeetingMinutes[] @relation("MinutesCreator")
minutesApproved MeetingMinutes[] @relation("MinutesApprover")
actionItemsAssigned ActionItem[] @relation("ActionItemAssignee")
actionItemsCreated ActionItem[] @relation("ActionItemCreator")
// Referral system
inviteCodesCreated InviteCode[] @relation("InviteCodesCreated")
referralsMade Referral[] @relation("ReferralsMade")
@@ -736,6 +747,9 @@ model Shift {
// Scheduling poll conversion
convertedFromPoll SchedulingPoll? @relation("PollConvertedShift")
// Meeting agenda
agenda MeetingAgenda? @relation("ShiftAgenda")
@@index([cutId])
@@index([seriesId])
@@map("shifts")
@@ -750,6 +764,7 @@ enum SignupSource {
AUTHENTICATED
PUBLIC
ADMIN
POLL_CONVERSION
}
model ShiftSignup {
@@ -4168,6 +4183,7 @@ enum ContactSource {
SHIFT_SIGNUP
SMS_CONTACT
DONATION
POLL_VOTE
MANUAL
}
@@ -4194,6 +4210,7 @@ enum ContactActivityType {
PROFILE_SELF_EDIT
PROFILE_PHOTO_UPDATED
USER_LOGIN
POLL_VOTED
}
model Contact {
@@ -4203,6 +4220,7 @@ model Contact {
lastName String?
email String?
phone String?
pronouns String?
// CRM data
tags Json @default("[]") // String array
@@ -4247,6 +4265,8 @@ model Contact {
connectionsTo ContactConnection[] @relation("ConnectionTo")
activities ContactActivity[]
smsConversations SmsConversation[] @relation("ContactSmsConversations")
pollVotes SchedulingPollVote[] @relation("PollVoteContact")
participantNeeds ParticipantNeeds? @relation("ContactParticipantNeeds")
@@index([email])
@@index([phone])
@@ -4404,7 +4424,16 @@ model SchedulingPoll {
convertedShiftId String? @unique @map("converted_shift_id")
convertedShift Shift? @relation("PollConvertedShift", fields: [convertedShiftId], references: [id], onDelete: SetNull)
convertedGancioEventId Int? @map("converted_gancio_event_id")
convertedCalendarItemId String? @map("converted_calendar_item_id")
votingDeadline DateTime? @map("voting_deadline")
autoFinalize Boolean @default(false) @map("auto_finalize")
autoFinalizeThreshold Int? @map("auto_finalize_threshold")
autoConvertToCalendar Boolean @default(false) @map("auto_convert_to_calendar")
autoConvertToGancio Boolean @default(false) @map("auto_convert_to_gancio")
autoConvertToShift Boolean @default(false) @map("auto_convert_to_shift")
tieBreaker String @default("earliest") @map("tie_breaker")
autoEnrollVoters Boolean @default(true) @map("auto_enroll_voters")
autoFinalizeJobId String? @map("auto_finalize_job_id")
allowAnonymous Boolean @default(true) @map("allow_anonymous")
isPrivate Boolean @default(false) @map("is_private")
notifyOnVote Boolean @default(true) @map("notify_on_vote")
@@ -4416,6 +4445,7 @@ model SchedulingPoll {
options SchedulingPollOption[] @relation("PollOptions")
votes SchedulingPollVote[] @relation("PollVotes")
comments SchedulingPollComment[] @relation("PollComments")
agenda MeetingAgenda? @relation("PollAgenda")
@@index([createdByUserId])
@@index([status])
@@ -4451,6 +4481,8 @@ model SchedulingPollVote {
voterName String @map("voter_name")
voterEmail String? @map("voter_email")
voterToken String? @map("voter_token") // anonymous edit access (cuid)
contactId String? @map("contact_id")
contact Contact? @relation("PollVoteContact", fields: [contactId], references: [id], onDelete: SetNull)
value PollVoteValue
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@ -4458,6 +4490,7 @@ model SchedulingPollVote {
@@unique([optionId, userId])
@@unique([optionId, voterToken])
@@index([pollId])
@@index([contactId])
@@map("scheduling_poll_votes")
}
@@ -4906,6 +4939,7 @@ enum CalendarShowDetailsTo {
enum CalendarItemSource {
MANUAL
ICS_FEED
POLL
}
enum CalendarRecurrenceFrequency {
@@ -5131,3 +5165,119 @@ model DocCollabState {
@@map("doc_collab_state")
}
// ============================================================================
// PARTICIPANT NEEDS
// ============================================================================
model ParticipantNeeds {
id String @id @default(cuid())
userId String? @unique @map("user_id")
user User? @relation("UserParticipantNeeds", fields: [userId], references: [id], onDelete: SetNull)
contactId String? @unique @map("contact_id")
contact Contact? @relation("ContactParticipantNeeds", fields: [contactId], references: [id], onDelete: SetNull)
// Accessibility
needsWheelchair Boolean @default(false) @map("needs_wheelchair")
needsGroundFloor Boolean @default(false) @map("needs_ground_floor")
needsHearingLoop Boolean @default(false) @map("needs_hearing_loop")
needsSignLanguage Boolean @default(false) @map("needs_sign_language")
otherAccessibility String? @db.Text @map("other_accessibility")
// Dietary
isVegan Boolean @default(false) @map("is_vegan")
isVegetarian Boolean @default(false) @map("is_vegetarian")
isGlutenFree Boolean @default(false) @map("is_gluten_free")
isHalal Boolean @default(false) @map("is_halal")
isKosher Boolean @default(false) @map("is_kosher")
hasNutAllergy Boolean @default(false) @map("has_nut_allergy")
otherDietary String? @db.Text @map("other_dietary")
// Care barriers
needsChildcare Boolean @default(false) @map("needs_childcare")
childcareDetails String? @db.Text @map("childcare_details")
needsTransportation Boolean @default(false) @map("needs_transportation")
transportationNotes String? @db.Text @map("transportation_notes")
// Communication
preferredLanguage String? @default("en") @map("preferred_language")
needsTranslation Boolean @default(false) @map("needs_translation")
translationLanguage String? @map("translation_language")
// Consent
visibilityConsent String @default("organizer_only") @map("visibility_consent")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("participant_needs")
}
// ============================================================================
// MEETING AGENDAS & ACTION ITEMS
// ============================================================================
model MeetingAgenda {
id String @id @default(cuid())
shiftId String? @unique @map("shift_id")
shift Shift? @relation("ShiftAgenda", fields: [shiftId], references: [id], onDelete: SetNull)
pollId String? @unique @map("poll_id")
poll SchedulingPoll? @relation("PollAgenda", fields: [pollId], references: [id], onDelete: SetNull)
title String
items Json @default("[]") // Array<{ id, title, durationMinutes, presenterUserId, notes, order }>
status String @default("draft") // "draft" | "active" | "completed"
minutes MeetingMinutes?
actionItems ActionItem[]
createdByUserId String @map("created_by_user_id")
createdBy User @relation("AgendaCreator", fields: [createdByUserId], references: [id])
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("meeting_agendas")
}
model MeetingMinutes {
id String @id @default(cuid())
agendaId String @unique @map("agenda_id")
agenda MeetingAgenda @relation(fields: [agendaId], references: [id], onDelete: Cascade)
notes String @db.Text
decisions Json @default("[]") // Array<{ id, text, passed: boolean }>
attendees Json @default("[]") // Array<{ name, pronouns, userId? }>
approvedAt DateTime? @map("approved_at")
approvedByUserId String? @map("approved_by_user_id")
approvedBy User? @relation("MinutesApprover", fields: [approvedByUserId], references: [id], onDelete: SetNull)
createdByUserId String @map("created_by_user_id")
createdBy User @relation("MinutesCreator", fields: [createdByUserId], references: [id])
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("meeting_minutes")
}
model ActionItem {
id String @id @default(cuid())
agendaId String? @map("agenda_id")
agenda MeetingAgenda? @relation(fields: [agendaId], references: [id], onDelete: SetNull)
title String
description String? @db.Text
assigneeUserId String? @map("assignee_user_id")
assignee User? @relation("ActionItemAssignee", fields: [assigneeUserId], references: [id], onDelete: SetNull)
dueDate DateTime? @map("due_date")
status String @default("open") // "open" | "in_progress" | "done" | "blocked"
priority String @default("normal") // "low" | "normal" | "high" | "urgent"
completedAt DateTime? @map("completed_at")
createdByUserId String @map("created_by_user_id")
createdBy User @relation("ActionItemCreator", fields: [createdByUserId], references: [id])
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@index([assigneeUserId, status])
@@index([dueDate])
@@map("action_items")
}