From 3fc67cd81a93a923bbe091c2b39f11c31a769304 Mon Sep 17 00:00:00 2001 From: bunker-admin Date: Fri, 10 Apr 2026 21:26:56 -0600 Subject: [PATCH] Add ActionCampaign + Document models for volunteer dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Foundation schema for the FAFC-style volunteer dashboard. Adds: - ActionCampaign / ActionStep / ActionStepCompletion (stacked-action mini-campaigns where steps reference existing entities like videos, petitions, ticketed events; completion is detected at query time against the per-user model for each step kind) - Document model for downloadable resources (PDFs etc.) — Photo's EXIF/sharp pipeline can't host non-image files - Shift.kind discriminator (ShiftKind enum) so training shifts can surface separately on the dashboard - TicketedEvent.featured for the "Take Action" CTA tile Bunker Admin --- .../migration.sql | 130 + api/prisma/schema.prisma | 4060 +++++++++-------- 2 files changed, 2228 insertions(+), 1962 deletions(-) create mode 100644 api/prisma/migrations/20260410200000_volunteer_dashboard_action_campaigns/migration.sql diff --git a/api/prisma/migrations/20260410200000_volunteer_dashboard_action_campaigns/migration.sql b/api/prisma/migrations/20260410200000_volunteer_dashboard_action_campaigns/migration.sql new file mode 100644 index 00000000..5c09f268 --- /dev/null +++ b/api/prisma/migrations/20260410200000_volunteer_dashboard_action_campaigns/migration.sql @@ -0,0 +1,130 @@ + +-- CreateEnum +CREATE TYPE "ShiftKind" AS ENUM ('CANVASS', 'TRAINING', 'EVENT_STAFFING', 'PHONE_BANK', 'OTHER'); + +-- CreateEnum +CREATE TYPE "ActionStepKind" AS ENUM ('WATCH_VIDEO', 'SUBMIT_INFLUENCE', 'SIGN_PETITION', 'RSVP_EVENT', 'SIGNUP_SHIFT', 'JOIN_CHALLENGE', 'VISIT_LINK', 'CUSTOM'); + +-- CreateEnum +CREATE TYPE "ActionStepCompletionSource" AS ENUM ('AUTO', 'SELF_REPORTED'); + +-- AlterTable +ALTER TABLE "shifts" ADD COLUMN "kind" "ShiftKind" NOT NULL DEFAULT 'CANVASS'; + +-- AlterTable +ALTER TABLE "ticketed_events" ADD COLUMN "featured" BOOLEAN NOT NULL DEFAULT false; + +-- CreateTable +CREATE TABLE "action_campaigns" ( + "id" TEXT NOT NULL, + "slug" TEXT NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT, + "reward_text" TEXT, + "is_active" BOOLEAN NOT NULL DEFAULT false, + "starts_at" TIMESTAMP(3), + "ends_at" TIMESTAMP(3), + "min_steps_for_reward" INTEGER, + "created_by_user_id" TEXT, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "action_campaigns_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "action_steps" ( + "id" TEXT NOT NULL, + "campaign_id" TEXT NOT NULL, + "order" INTEGER NOT NULL, + "kind" "ActionStepKind" NOT NULL, + "label" TEXT NOT NULL, + "description" TEXT, + "target_id" TEXT, + "target_url" TEXT, + "auto_complete" BOOLEAN NOT NULL DEFAULT true, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "action_steps_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "action_step_completions" ( + "id" TEXT NOT NULL, + "user_id" TEXT NOT NULL, + "step_id" TEXT NOT NULL, + "source" "ActionStepCompletionSource" NOT NULL DEFAULT 'AUTO', + "completed_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "action_step_completions_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "documents" ( + "id" TEXT NOT NULL, + "path" TEXT NOT NULL, + "filename" TEXT NOT NULL, + "original_filename" TEXT, + "title" TEXT, + "description" TEXT, + "mime_type" TEXT NOT NULL, + "file_size" BIGINT, + "page_count" INTEGER, + "thumbnail_path" TEXT, + "category" TEXT, + "tags" JSONB, + "is_published" BOOLEAN NOT NULL DEFAULT true, + "position" INTEGER DEFAULT 0, + "uploader_id" TEXT, + "download_count" INTEGER NOT NULL DEFAULT 0, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "documents_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "action_campaigns_slug_key" ON "action_campaigns"("slug"); + +-- CreateIndex +CREATE INDEX "idx_action_campaigns_active" ON "action_campaigns"("is_active"); + +-- CreateIndex +CREATE INDEX "idx_action_steps_campaign" ON "action_steps"("campaign_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "action_steps_campaign_id_order_key" ON "action_steps"("campaign_id", "order"); + +-- CreateIndex +CREATE INDEX "idx_action_step_completions_user" ON "action_step_completions"("user_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "action_step_completions_user_id_step_id_key" ON "action_step_completions"("user_id", "step_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "documents_path_key" ON "documents"("path"); + +-- CreateIndex +CREATE INDEX "idx_documents_published" ON "documents"("is_published"); + +-- CreateIndex +CREATE INDEX "idx_documents_category" ON "documents"("category"); + +-- CreateIndex +CREATE INDEX "idx_documents_created_at" ON "documents"("created_at"); + +-- AddForeignKey +ALTER TABLE "action_campaigns" ADD CONSTRAINT "action_campaigns_created_by_user_id_fkey" FOREIGN KEY ("created_by_user_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "action_steps" ADD CONSTRAINT "action_steps_campaign_id_fkey" FOREIGN KEY ("campaign_id") REFERENCES "action_campaigns"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "action_step_completions" ADD CONSTRAINT "action_step_completions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "action_step_completions" ADD CONSTRAINT "action_step_completions_step_id_fkey" FOREIGN KEY ("step_id") REFERENCES "action_steps"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "documents" ADD CONSTRAINT "documents_uploader_id_fkey" FOREIGN KEY ("uploader_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE; + diff --git a/api/prisma/schema.prisma b/api/prisma/schema.prisma index 55377b18..c1276a22 100644 --- a/api/prisma/schema.prisma +++ b/api/prisma/schema.prisma @@ -45,188 +45,193 @@ enum UserCreatedVia { } model User { - id String @id @default(cuid()) - email String @unique - password String // bcrypt hashed + id String @id @default(cuid()) + email String @unique + 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) - permissions Json? // Per-app granular permissions - createdVia UserCreatedVia @default(STANDARD) - expiresAt DateTime? // For temp users + role UserRole @default(USER) + roles Json @default("[]") // Array of UserRole strings for multi-role support + status UserStatus @default(ACTIVE) + permissions Json? // Per-app granular permissions + createdVia UserCreatedVia @default(STANDARD) + expiresAt DateTime? // For temp users expireDays Int? lastLoginAt DateTime? - emailVerified Boolean @default(false) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + emailVerified Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt - refreshTokens RefreshToken[] - campaignsCreated Campaign[] @relation("CampaignCreator") - campaignsReviewed Campaign[] @relation("CampaignReviewer") - campaignEmails CampaignEmail[] @relation("CampaignEmailSender") - responses RepresentativeResponse[] @relation("ResponseSubmitter") - responseUpvotes ResponseUpvote[] - shiftSignups ShiftSignup[] - locationsCreated Location[] @relation("LocationCreator") - locationsUpdated Location[] @relation("LocationUpdater") - addressesCreated Address[] @relation("AddressCreator") - addressesUpdated Address[] @relation("AddressUpdater") - locationEdits LocationHistory[] @relation("LocationHistoryUser") - cutsCreated Cut[] @relation("CutCreator") - canvassVisits CanvassVisit[] @relation("CanvassVisitor") - canvassSessions CanvassSession[] @relation("CanvassSessions") - trackingSessions TrackingSession[] @relation("TrackingSessions") - templatesCreated EmailTemplate[] @relation("TemplatesCreated") - templatesUpdated EmailTemplate[] @relation("TemplatesUpdated") - templateVersionsCreated EmailTemplateVersion[] @relation("TemplateVersionsCreated") - templateTestsSent EmailTemplateTestLog[] @relation("TemplateTestsSent") + refreshTokens RefreshToken[] + campaignsCreated Campaign[] @relation("CampaignCreator") + campaignsReviewed Campaign[] @relation("CampaignReviewer") + campaignEmails CampaignEmail[] @relation("CampaignEmailSender") + responses RepresentativeResponse[] @relation("ResponseSubmitter") + responseUpvotes ResponseUpvote[] + shiftSignups ShiftSignup[] + locationsCreated Location[] @relation("LocationCreator") + locationsUpdated Location[] @relation("LocationUpdater") + addressesCreated Address[] @relation("AddressCreator") + addressesUpdated Address[] @relation("AddressUpdater") + locationEdits LocationHistory[] @relation("LocationHistoryUser") + cutsCreated Cut[] @relation("CutCreator") + canvassVisits CanvassVisit[] @relation("CanvassVisitor") + canvassSessions CanvassSession[] @relation("CanvassSessions") + trackingSessions TrackingSession[] @relation("TrackingSessions") + templatesCreated EmailTemplate[] @relation("TemplatesCreated") + templatesUpdated EmailTemplate[] @relation("TemplatesUpdated") + templateVersionsCreated EmailTemplateVersion[] @relation("TemplateVersionsCreated") + templateTestsSent EmailTemplateTestLog[] @relation("TemplateTestsSent") // Media API relations - videosUploaded Video[] @relation("VideoUploader") - videosLocked Video[] @relation("VideoLocker") - videoViews VideoView[] @relation("VideoViews") - videoScheduleHistory VideoScheduleHistory[] @relation("VideoScheduleHistory") - sessions Session[] @relation("SessionUser") - comments Comment[] @relation("CommentUser") - authTokens AuthToken[] @relation("AuthTokenUser") - sessionBansMade SessionBan[] @relation("SessionBanner") - commentModerations CommentModeration[] @relation("CommentModerator") - emailVerificationTokens EmailVerificationToken[] @relation("EmailVerificationTokens") - passwordResetTokens PasswordResetToken[] @relation("PasswordResetTokens") - emailChangeTokens EmailChangeToken[] @relation("EmailChangeTokens") - achievements UserAchievement[] @relation("UserAchievements") - stats UserStats? @relation("UserStats") - finishes UserFinish[] @relation("UserFinishes") - videoReactions VideoReaction[] @relation("VideoReactions") - highlightCooldowns HighlightCooldown[] @relation("HighlightCooldowns") - dailyActivity UserDailyActivity[] @relation("UserDailyActivity") - chatThreadReadStatus ChatThreadReadStatus[] @relation("ChatThreadReadStatus") - moderationWordLists ModerationWordList[] @relation("ModerationWordListCreator") - contentReportsSubmitted ContentReport[] @relation("ContentReportUser") - contentReportsResolved ContentReport[] @relation("ContentReportResolver") - playlists Playlist[] @relation("UserPlaylists") - featuredPlaylists FeaturedPlaylist[] @relation("FeaturedPlaylistFeaturer") - adImpressions AdImpression[] @relation("AdImpressionUser") - adClicks AdClick[] @relation("AdClickUser") - friendships Friendship[] @relation("UserFriendships") - friends Friendship[] @relation("UserFriends") - blocks UserBlock[] @relation("UserBlocks") - blockedBy UserBlock[] @relation("UserBlockedBy") - pokesSent Poke[] @relation("PokesSent") - pokesReceived Poke[] @relation("PokesReceived") - recommendationsSent VideoRecommendation[] @relation("RecommendationsSent") - recommendationsReceived VideoRecommendation[] @relation("RecommendationsReceived") - presence UserPresence? @relation("UserPresence") - galleryImages UserGalleryImage[] @relation("UserGalleryImages") - socialLinks UserSocialLink[] @relation("UserSocialLinks") - privacySettings PrivacySettings? @relation("PrivacySettings") - closeFriends CloseFriend[] @relation("CloseFriends") - closeFriendOf CloseFriend[] @relation("CloseFriendOf") - socialGroupMemberships SocialGroupMember[] @relation("SocialGroupMember") - uploads UserUpload[] @relation("UserUploads") - uploadReviews UserUpload[] @relation("UserUploadReviewer") - uploadInvites UploadInvite[] @relation("UploadInviteCreator") - tagPreferences UserTagPreference[] @relation("UserTagPreferences") - performerDiscrepancies PerformerDiscrepancy[] @relation("PerformerDiscrepancyResolver") - watchPartiesHosted WatchPartySession[] @relation("WatchPartyHost") - watchPartyParticipations WatchPartyParticipant[] @relation("WatchPartyParticipant") - watchPartyChatMessages WatchPartyChatMessage[] @relation("WatchPartyChatUser") - watchPartyReactions WatchPartyReaction[] @relation("WatchPartyReactionUser") - watchPartyInvitesSent WatchPartyInvite[] @relation("WatchPartyInviter") - watchPartyInvitesReceived WatchPartyInvite[] @relation("WatchPartyInvitee") - subscriptions UserSubscription[] @relation("UserSubscriptions") - invoices Invoice[] @relation("UserInvoices") - payments Payment[] @relation("UserPayments") - paymentAudits PaymentAuditLog[] @relation("PaymentAuditUser") - orders Order[] @relation("UserOrders") - notifications Notification[] @relation("UserNotifications") - notificationPreferences NotificationPreferences? @relation("NotificationPreferences") + videosUploaded Video[] @relation("VideoUploader") + videosLocked Video[] @relation("VideoLocker") + videoViews VideoView[] @relation("VideoViews") + videoScheduleHistory VideoScheduleHistory[] @relation("VideoScheduleHistory") + sessions Session[] @relation("SessionUser") + comments Comment[] @relation("CommentUser") + authTokens AuthToken[] @relation("AuthTokenUser") + sessionBansMade SessionBan[] @relation("SessionBanner") + commentModerations CommentModeration[] @relation("CommentModerator") + emailVerificationTokens EmailVerificationToken[] @relation("EmailVerificationTokens") + passwordResetTokens PasswordResetToken[] @relation("PasswordResetTokens") + emailChangeTokens EmailChangeToken[] @relation("EmailChangeTokens") + achievements UserAchievement[] @relation("UserAchievements") + stats UserStats? @relation("UserStats") + finishes UserFinish[] @relation("UserFinishes") + videoReactions VideoReaction[] @relation("VideoReactions") + highlightCooldowns HighlightCooldown[] @relation("HighlightCooldowns") + dailyActivity UserDailyActivity[] @relation("UserDailyActivity") + chatThreadReadStatus ChatThreadReadStatus[] @relation("ChatThreadReadStatus") + moderationWordLists ModerationWordList[] @relation("ModerationWordListCreator") + contentReportsSubmitted ContentReport[] @relation("ContentReportUser") + contentReportsResolved ContentReport[] @relation("ContentReportResolver") + playlists Playlist[] @relation("UserPlaylists") + featuredPlaylists FeaturedPlaylist[] @relation("FeaturedPlaylistFeaturer") + adImpressions AdImpression[] @relation("AdImpressionUser") + adClicks AdClick[] @relation("AdClickUser") + friendships Friendship[] @relation("UserFriendships") + friends Friendship[] @relation("UserFriends") + blocks UserBlock[] @relation("UserBlocks") + blockedBy UserBlock[] @relation("UserBlockedBy") + pokesSent Poke[] @relation("PokesSent") + pokesReceived Poke[] @relation("PokesReceived") + recommendationsSent VideoRecommendation[] @relation("RecommendationsSent") + recommendationsReceived VideoRecommendation[] @relation("RecommendationsReceived") + presence UserPresence? @relation("UserPresence") + galleryImages UserGalleryImage[] @relation("UserGalleryImages") + socialLinks UserSocialLink[] @relation("UserSocialLinks") + privacySettings PrivacySettings? @relation("PrivacySettings") + closeFriends CloseFriend[] @relation("CloseFriends") + closeFriendOf CloseFriend[] @relation("CloseFriendOf") + socialGroupMemberships SocialGroupMember[] @relation("SocialGroupMember") + uploads UserUpload[] @relation("UserUploads") + uploadReviews UserUpload[] @relation("UserUploadReviewer") + uploadInvites UploadInvite[] @relation("UploadInviteCreator") + tagPreferences UserTagPreference[] @relation("UserTagPreferences") + performerDiscrepancies PerformerDiscrepancy[] @relation("PerformerDiscrepancyResolver") + watchPartiesHosted WatchPartySession[] @relation("WatchPartyHost") + watchPartyParticipations WatchPartyParticipant[] @relation("WatchPartyParticipant") + watchPartyChatMessages WatchPartyChatMessage[] @relation("WatchPartyChatUser") + watchPartyReactions WatchPartyReaction[] @relation("WatchPartyReactionUser") + watchPartyInvitesSent WatchPartyInvite[] @relation("WatchPartyInviter") + watchPartyInvitesReceived WatchPartyInvite[] @relation("WatchPartyInvitee") + subscriptions UserSubscription[] @relation("UserSubscriptions") + invoices Invoice[] @relation("UserInvoices") + payments Payment[] @relation("UserPayments") + paymentAudits PaymentAuditLog[] @relation("PaymentAuditUser") + orders Order[] @relation("UserOrders") + notifications Notification[] @relation("UserNotifications") + notificationPreferences NotificationPreferences? @relation("NotificationPreferences") // Photo gallery relations - photosUploaded Photo[] @relation("PhotoUploader") - albumsCreated PhotoAlbum[] @relation("AlbumCreator") - photoComments PhotoComment[] @relation("PhotoCommentUser") + photosUploaded Photo[] @relation("PhotoUploader") + albumsCreated PhotoAlbum[] @relation("AlbumCreator") + photoComments PhotoComment[] @relation("PhotoCommentUser") // SMS campaign relations - smsContactListsCreated SmsContactList[] @relation("SmsContactListCreator") - smsCampaignsCreated SmsCampaign[] @relation("SmsCampaignCreator") - smsTemplatesCreated SmsMessageTemplate[] @relation("SmsTemplateCreator") + smsContactListsCreated SmsContactList[] @relation("SmsContactListCreator") + smsCampaignsCreated SmsCampaign[] @relation("SmsCampaignCreator") + smsTemplatesCreated SmsMessageTemplate[] @relation("SmsTemplateCreator") // Donation pages - donationPagesCreated DonationPage[] @relation("DonationPageCreator") + donationPagesCreated DonationPage[] @relation("DonationPageCreator") // Meetings (Jitsi) - meetingsCreated Meeting[] @relation("MeetingCreator") + meetingsCreated Meeting[] @relation("MeetingCreator") // People CRM - contact Contact? @relation("UserContact") + contact Contact? @relation("UserContact") // Scheduling polls - schedulingPollsCreated SchedulingPoll[] @relation("PollCreator") - schedulingPollVotes SchedulingPollVote[] @relation("PollVoter") - schedulingPollComments SchedulingPollComment[] @relation("PollCommenter") + schedulingPollsCreated SchedulingPoll[] @relation("PollCreator") + schedulingPollVotes SchedulingPollVote[] @relation("PollVoter") + schedulingPollComments SchedulingPollComment[] @relation("PollCommenter") // Straw polls - strawPollsCreated StrawPoll[] @relation("StrawPollCreator") - strawPollVotes StrawPollVote[] @relation("StrawPollVoter") - strawPollComments StrawPollComment[] @relation("StrawPollCommenter") - strawPollChallengesSent StrawPollChallenge[] @relation("StrawPollChallenger") - strawPollChallengesReceived StrawPollChallenge[] @relation("StrawPollChallenged") + strawPollsCreated StrawPoll[] @relation("StrawPollCreator") + strawPollVotes StrawPollVote[] @relation("StrawPollVoter") + strawPollComments StrawPollComment[] @relation("StrawPollCommenter") + strawPollChallengesSent StrawPollChallenge[] @relation("StrawPollChallenger") + strawPollChallengesReceived StrawPollChallenge[] @relation("StrawPollChallenged") // Participant needs - participantNeeds ParticipantNeeds? @relation("UserParticipantNeeds") + 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") + 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") - referredBy Referral? @relation("ReferredBy") + inviteCodesCreated InviteCode[] @relation("InviteCodesCreated") + referralsMade Referral[] @relation("ReferralsMade") + referredBy Referral? @relation("ReferredBy") // Impact Stories - impactStoriesCreated ImpactStory[] @relation("ImpactStoryCreator") + impactStoriesCreated ImpactStory[] @relation("ImpactStoryCreator") // Volunteer Spotlight - spotlights VolunteerSpotlight[] @relation("SpotlightUser") - spotlightNominations VolunteerSpotlight[] @relation("SpotlightNominator") - spotlightApprovals VolunteerSpotlight[] @relation("SpotlightApprover") + spotlights VolunteerSpotlight[] @relation("SpotlightUser") + spotlightNominations VolunteerSpotlight[] @relation("SpotlightNominator") + spotlightApprovals VolunteerSpotlight[] @relation("SpotlightApprover") // Team Challenges - challengesCreated Challenge[] @relation("ChallengesCreated") - challengeTeamsCaptained ChallengeTeam[] @relation("ChallengeTeamsCaptained") - challengeParticipations ChallengeTeamMember[] @relation("ChallengeParticipations") + challengesCreated Challenge[] @relation("ChallengesCreated") + challengeTeamsCaptained ChallengeTeam[] @relation("ChallengeTeamsCaptained") + challengeParticipations ChallengeTeamMember[] @relation("ChallengeParticipations") // Ticketed Events - ticketedEventsCreated TicketedEvent[] @relation("EventCreator") - ticketsHeld Ticket[] @relation("TicketHolder") - checkInsMade CheckIn[] @relation("CheckInUser") + ticketedEventsCreated TicketedEvent[] @relation("EventCreator") + ticketsHeld Ticket[] @relation("TicketHolder") + checkInsMade CheckIn[] @relation("CheckInUser") // Social Calendar - calendarLayers CalendarLayer[] @relation("CalendarLayerOwner") - calendarItems CalendarItem[] @relation("CalendarItemOwner") - calendarFeeds CalendarFeed[] @relation("CalendarFeedOwner") - sharedCalendarViewsOwned SharedCalendarView[] @relation("SharedViewOwner") - sharedCalendarMemberships SharedCalendarMember[] @relation("SharedViewMember") - sharedViewComments SharedViewComment[] @relation("SharedViewCommentUser") - sharedViewReactions SharedViewReaction[] @relation("SharedViewReactionUser") - calendarExportTokens CalendarExportToken[] @relation("CalendarExportTokenOwner") + calendarLayers CalendarLayer[] @relation("CalendarLayerOwner") + calendarItems CalendarItem[] @relation("CalendarItemOwner") + calendarFeeds CalendarFeed[] @relation("CalendarFeedOwner") + sharedCalendarViewsOwned SharedCalendarView[] @relation("SharedViewOwner") + sharedCalendarMemberships SharedCalendarMember[] @relation("SharedViewMember") + sharedViewComments SharedViewComment[] @relation("SharedViewCommentUser") + sharedViewReactions SharedViewReaction[] @relation("SharedViewReactionUser") + calendarExportTokens CalendarExportToken[] @relation("CalendarExportTokenOwner") // Docs access & sharing - docAccessPoliciesCreated DocAccessPolicy[] @relation("DocAccessPolicyCreator") - docShareLinksCreated DocShareLink[] @relation("DocShareLinkCreator") - docWatches DocWatch[] @relation("DocWatcher") + docAccessPoliciesCreated DocAccessPolicy[] @relation("DocAccessPolicyCreator") + docShareLinksCreated DocShareLink[] @relation("DocShareLinkCreator") + docWatches DocWatch[] @relation("DocWatcher") // Petitions - petitionsCreated Petition[] @relation("PetitionCreator") - petitionsReviewed Petition[] @relation("PetitionReviewer") + petitionsCreated Petition[] @relation("PetitionCreator") + petitionsReviewed Petition[] @relation("PetitionReviewer") + + // Volunteer dashboard — action campaigns + actionCampaignsCreated ActionCampaign[] @relation("ActionCampaignCreator") + actionStepCompletions ActionStepCompletion[] @relation("ActionStepCompleter") + documentsUploaded Document[] @relation("DocumentUploader") @@map("users") } @@ -269,58 +274,58 @@ enum GovernmentLevel { } model Campaign { - id String @id @default(cuid()) - slug String @unique - title String - description String? @db.Text - emailSubject String - emailBody String @db.Text - callToAction String? @db.Text - coverPhoto String? - coverVideoId Int? - status CampaignStatus @default(DRAFT) + id String @id @default(cuid()) + slug String @unique + title String + description String? @db.Text + emailSubject String + emailBody String @db.Text + callToAction String? @db.Text + coverPhoto String? + coverVideoId Int? + status CampaignStatus @default(DRAFT) // Feature flags - allowSmtpEmail Boolean @default(true) - allowMailtoLink Boolean @default(true) - collectUserInfo Boolean @default(true) - showEmailCount Boolean @default(true) - showCallCount Boolean @default(true) - allowEmailEditing Boolean @default(false) - allowCustomRecipients Boolean @default(false) - showResponseWall Boolean @default(false) - highlightCampaign Boolean @default(false) + allowSmtpEmail Boolean @default(true) + allowMailtoLink Boolean @default(true) + collectUserInfo Boolean @default(true) + showEmailCount Boolean @default(true) + showCallCount Boolean @default(true) + allowEmailEditing Boolean @default(false) + allowCustomRecipients Boolean @default(false) + showResponseWall Boolean @default(false) + highlightCampaign Boolean @default(false) // Targeting targetGovernmentLevels GovernmentLevel[] // Creator - createdByUserId String? - createdByUser User? @relation("CampaignCreator", fields: [createdByUserId], references: [id], onDelete: SetNull) - createdByUserEmail String? - createdByUserName String? + createdByUserId String? + createdByUser User? @relation("CampaignCreator", fields: [createdByUserId], references: [id], onDelete: SetNull) + createdByUserEmail String? + createdByUserName String? // User-generated campaign moderation - isUserGenerated Boolean @default(false) - moderationStatus CampaignModerationStatus? - reviewedByUserId String? - reviewedByUser User? @relation("CampaignReviewer", fields: [reviewedByUserId], references: [id], onDelete: SetNull) - reviewedAt DateTime? - rejectionReason String? @db.Text - moderationNotes String? @db.Text + isUserGenerated Boolean @default(false) + moderationStatus CampaignModerationStatus? + reviewedByUserId String? + reviewedByUser User? @relation("CampaignReviewer", fields: [reviewedByUserId], references: [id], onDelete: SetNull) + reviewedAt DateTime? + rejectionReason String? @db.Text + moderationNotes String? @db.Text - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt - emails CampaignEmail[] - responses RepresentativeResponse[] - customRecipients CustomRecipient[] - calls Call[] - smsCampaigns SmsCampaign[] @relation("SmsCampaigns") - stories ImpactStory[] @relation("CampaignStories") - milestones CampaignMilestone[] @relation("CampaignMilestones") - donationOrders Order[] @relation("CampaignDonations") - petitions Petition[] @relation("PetitionLinkedCampaign") + emails CampaignEmail[] + responses RepresentativeResponse[] + customRecipients CustomRecipient[] + calls Call[] + smsCampaigns SmsCampaign[] @relation("SmsCampaigns") + stories ImpactStory[] @relation("CampaignStories") + milestones CampaignMilestone[] @relation("CampaignMilestones") + donationOrders Order[] @relation("CampaignDonations") + petitions Petition[] @relation("PetitionLinkedCampaign") @@index([moderationStatus]) @@index([isUserGenerated]) @@ -347,64 +352,64 @@ enum PetitionSignatureStatus { } model Petition { - id String @id @default(cuid()) - slug String @unique - title String - description String? @db.Text + id String @id @default(cuid()) + slug String @unique + title String + description String? @db.Text // Goal and progress - signatureGoal Int? - showProgress Boolean @default(true) - showSignatureCount Boolean @default(true) - showSignerNames Boolean @default(true) - signatureCountOffset Int @default(0) + signatureGoal Int? + showProgress Boolean @default(true) + showSignatureCount Boolean @default(true) + showSignerNames Boolean @default(true) + signatureCountOffset Int @default(0) // Form fields - requireName Boolean @default(true) - requireEmail Boolean @default(true) - requirePostalCode Boolean @default(false) - requirePhone Boolean @default(false) - allowComment Boolean @default(true) - commentLabel String? + requireName Boolean @default(true) + requireEmail Boolean @default(true) + requirePostalCode Boolean @default(false) + requirePhone Boolean @default(false) + allowComment Boolean @default(true) + commentLabel String? // Email confirmation - requireEmailConfirmation Boolean @default(false) - confirmationEmailSubject String? - confirmationEmailBody String? @db.Text + requireEmailConfirmation Boolean @default(false) + confirmationEmailSubject String? + confirmationEmailBody String? @db.Text // Presentation - coverPhoto String? - coverVideoId Int? - callToAction String? @db.Text - thankYouMessage String? @db.Text - highlightPetition Boolean @default(false) + coverPhoto String? + coverVideoId Int? + callToAction String? @db.Text + thankYouMessage String? @db.Text + highlightPetition Boolean @default(false) // Linked campaign (post-sign CTA) - linkedCampaignId String? - linkedCampaign Campaign? @relation("PetitionLinkedCampaign", fields: [linkedCampaignId], references: [id], onDelete: SetNull) + linkedCampaignId String? + linkedCampaign Campaign? @relation("PetitionLinkedCampaign", fields: [linkedCampaignId], references: [id], onDelete: SetNull) // Status and moderation - status PetitionStatus @default(DRAFT) - isUserGenerated Boolean @default(false) - moderationStatus CampaignModerationStatus? - rejectionReason String? @db.Text - moderationNotes String? @db.Text + status PetitionStatus @default(DRAFT) + isUserGenerated Boolean @default(false) + moderationStatus CampaignModerationStatus? + rejectionReason String? @db.Text + moderationNotes String? @db.Text // Creator - createdByUserId String? - createdByUser User? @relation("PetitionCreator", fields: [createdByUserId], references: [id], onDelete: SetNull) - createdByUserEmail String? - createdByUserName String? + createdByUserId String? + createdByUser User? @relation("PetitionCreator", fields: [createdByUserId], references: [id], onDelete: SetNull) + createdByUserEmail String? + createdByUserName String? // Reviewer - reviewedByUserId String? - reviewedByUser User? @relation("PetitionReviewer", fields: [reviewedByUserId], references: [id], onDelete: SetNull) - reviewedAt DateTime? + reviewedByUserId String? + reviewedByUser User? @relation("PetitionReviewer", fields: [reviewedByUserId], references: [id], onDelete: SetNull) + reviewedAt DateTime? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt - signatures PetitionSignature[] + signatures PetitionSignature[] @@index([status]) @@index([isUserGenerated]) @@ -414,37 +419,37 @@ model Petition { } model PetitionSignature { - id String @id @default(cuid()) - petitionId String - petition Petition @relation(fields: [petitionId], references: [id], onDelete: Cascade) + id String @id @default(cuid()) + petitionId String + petition Petition @relation(fields: [petitionId], references: [id], onDelete: Cascade) // Signer info - signerName String? - signerEmail String? - signerPostalCode String? - signerPhone String? - signerComment String? @db.Text - isAnonymous Boolean @default(false) - displayName String? + signerName String? + signerEmail String? + signerPostalCode String? + signerPhone String? + signerComment String? @db.Text + isAnonymous Boolean @default(false) + displayName String? // Status and verification - status PetitionSignatureStatus @default(UNVERIFIED) - verificationToken String? @unique + status PetitionSignatureStatus @default(UNVERIFIED) + verificationToken String? @unique verificationSentAt DateTime? - verifiedAt DateTime? + verifiedAt DateTime? // CRM link - contactId String? - contact Contact? @relation("PetitionSignatureContact", fields: [contactId], references: [id], onDelete: SetNull) + contactId String? + contact Contact? @relation("PetitionSignatureContact", fields: [contactId], references: [id], onDelete: SetNull) // Geo (from IP via MaxMind) - signerIp String? - geoCountry String? - geoRegion String? - geoCity String? + signerIp String? + geoCountry String? + geoRegion String? + geoCity String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt @@unique([petitionId, signerEmail]) @@index([petitionId]) @@ -459,7 +464,7 @@ model PetitionSignature { // ============================================================================ model Representative { - id String @id @default(cuid()) + id String @id @default(cuid()) postalCode String name String? email String? @@ -469,8 +474,8 @@ model Representative { representativeSetName String? url String? photoUrl String? - offices Json? // JSON array of office contact info - cachedAt DateTime @default(now()) + offices Json? // JSON array of office contact info + cachedAt DateTime @default(now()) @@index([postalCode]) @@map("representatives") @@ -494,30 +499,30 @@ enum CampaignEmailStatus { } model CampaignEmail { - id String @id @default(cuid()) - campaignId String - campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade) - campaignSlug String + id String @id @default(cuid()) + campaignId String + campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade) + campaignSlug String // Sender info - userId String? - user User? @relation("CampaignEmailSender", fields: [userId], references: [id], onDelete: SetNull) - userEmail String? - userName String? - userPostalCode String? + userId String? + user User? @relation("CampaignEmailSender", fields: [userId], references: [id], onDelete: SetNull) + userEmail String? + userName String? + userPostalCode String? // Recipient info - recipientEmail String - recipientName String? - recipientTitle String? - recipientLevel GovernmentLevel? + recipientEmail String + recipientName String? + recipientTitle String? + recipientLevel GovernmentLevel? - emailMethod EmailMethod - subject String - message String @db.Text - status CampaignEmailStatus @default(SENT) - senderIp String? - sentAt DateTime @default(now()) + emailMethod EmailMethod + subject String + message String @db.Text + status CampaignEmailStatus @default(SENT) + senderIp String? + sentAt DateTime @default(now()) @@index([campaignId]) @@index([campaignSlug]) @@ -546,44 +551,44 @@ enum ResponseStatus { } model RepresentativeResponse { - id String @id @default(cuid()) - campaignId String - campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade) - campaignSlug String + id String @id @default(cuid()) + campaignId String + campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade) + campaignSlug String - representativeName String - representativeTitle String? - representativeLevel GovernmentLevel - representativeEmail String? + representativeName String + representativeTitle String? + representativeLevel GovernmentLevel + representativeEmail String? - responseType ResponseType - responseText String @db.Text - userComment String? @db.Text - screenshotUrl String? + responseType ResponseType + responseText String @db.Text + userComment String? @db.Text + screenshotUrl String? // Submitter info - submittedByUserId String? - submittedByUser User? @relation("ResponseSubmitter", fields: [submittedByUserId], references: [id], onDelete: SetNull) - submittedByName String? - submittedByEmail String? - isAnonymous Boolean @default(false) + submittedByUserId String? + submittedByUser User? @relation("ResponseSubmitter", fields: [submittedByUserId], references: [id], onDelete: SetNull) + submittedByName String? + submittedByEmail String? + isAnonymous Boolean @default(false) // Moderation - status ResponseStatus @default(PENDING) + status ResponseStatus @default(PENDING) // Verification - isVerified Boolean @default(false) - verificationToken String? - verificationSentAt DateTime? - verifiedAt DateTime? - verifiedBy String? + isVerified Boolean @default(false) + verificationToken String? + verificationSentAt DateTime? + verifiedAt DateTime? + verifiedBy String? - upvoteCount Int @default(0) - submittedIp String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + upvoteCount Int @default(0) + submittedIp String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt - upvotes ResponseUpvote[] + upvotes ResponseUpvote[] @@index([campaignId]) @@index([campaignSlug]) @@ -592,11 +597,11 @@ model RepresentativeResponse { } model ResponseUpvote { - id String @id @default(cuid()) + id String @id @default(cuid()) responseId String response RepresentativeResponse @relation(fields: [responseId], references: [id], onDelete: Cascade) userId String? - user User? @relation(fields: [userId], references: [id], onDelete: SetNull) + user User? @relation(fields: [userId], references: [id], onDelete: SetNull) userEmail String? upvotedIp String? @@ -632,13 +637,13 @@ model CustomRecipient { // ============================================================================ model PostalCodeCache { - id String @id @default(cuid()) - postalCode String @unique + id String @id @default(cuid()) + postalCode String @unique city String? province String? - centroidLat Decimal? @db.Decimal(10, 8) - centroidLng Decimal? @db.Decimal(11, 8) - lastUpdated DateTime @default(now()) + centroidLat Decimal? @db.Decimal(10, 8) + centroidLng Decimal? @db.Decimal(11, 8) + lastUpdated DateTime @default(now()) @@map("postal_code_cache") } @@ -726,39 +731,39 @@ enum BuildingType { } model Location { - id String @id @default(cuid()) - latitude Decimal @db.Decimal(10, 8) // Required (was nullable) - longitude Decimal @db.Decimal(11, 8) // Required (was nullable) + id String @id @default(cuid()) + latitude Decimal @db.Decimal(10, 8) // Required (was nullable) + longitude Decimal @db.Decimal(11, 8) // Required (was nullable) // Building-level data - address String // Base street address (no unit number) - postalCode String? - province String? - federalDistrict String? - buildingUse Int? // NAR BU_USE: 1=Residential, 2=Partial, 3=Non-Residential, 4=Unknown + address String // Base street address (no unit number) + postalCode String? + province String? + federalDistrict String? + buildingUse Int? // NAR BU_USE: 1=Residential, 2=Partial, 3=Non-Residential, 4=Unknown // NAR + building metadata - locGuid String? @unique - buildingType BuildingType @default(SINGLE_FAMILY) - totalUnits Int @default(1) - buildingNotes String? @db.Text // Access codes, manager contact, etc. + locGuid String? @unique + buildingType BuildingType @default(SINGLE_FAMILY) + totalUnits Int @default(1) + buildingNotes String? @db.Text // Access codes, manager contact, etc. // Geocoding - geocodeConfidence Int? // 0-100 - geocodeProvider GeocodeProvider? + geocodeConfidence Int? // 0-100 + geocodeProvider GeocodeProvider? // Audit - createdByUserId String? - createdByUser User? @relation("LocationCreator", fields: [createdByUserId], references: [id], onDelete: SetNull) - updatedByUserId String? - updatedByUser User? @relation("LocationUpdater", fields: [updatedByUserId], references: [id], onDelete: SetNull) + createdByUserId String? + createdByUser User? @relation("LocationCreator", fields: [createdByUserId], references: [id], onDelete: SetNull) + updatedByUserId String? + updatedByUser User? @relation("LocationUpdater", fields: [updatedByUserId], references: [id], onDelete: SetNull) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt // Relations - addresses Address[] - history LocationHistory[] + addresses Address[] + history LocationHistory[] @@index([latitude, longitude]) @@index([latitude]) @@ -768,37 +773,37 @@ model Location { } model Address { - id String @id @default(cuid()) - locationId String - location Location @relation(fields: [locationId], references: [id], onDelete: Cascade) + id String @id @default(cuid()) + locationId String + location Location @relation(fields: [locationId], references: [id], onDelete: Cascade) // Unit identification - unitNumber String? - addrGuid String? @unique // NAR ADDR_GUID + unitNumber String? + addrGuid String? @unique // NAR ADDR_GUID // Occupant/contact info (per-unit) - firstName String? - lastName String? - email String? - phone String? + firstName String? + lastName String? + email String? + phone String? // Canvassing data (per-unit) - supportLevel SupportLevel? - sign Boolean @default(false) - signSize String? - notes String? @db.Text + supportLevel SupportLevel? + sign Boolean @default(false) + signSize String? + notes String? @db.Text // Audit - createdByUserId String? - createdByUser User? @relation("AddressCreator", fields: [createdByUserId], references: [id], onDelete: SetNull) - updatedByUserId String? - updatedByUser User? @relation("AddressUpdater", fields: [updatedByUserId], references: [id], onDelete: SetNull) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdByUserId String? + createdByUser User? @relation("AddressCreator", fields: [createdByUserId], references: [id], onDelete: SetNull) + updatedByUserId String? + updatedByUser User? @relation("AddressUpdater", fields: [updatedByUserId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt // Relations - canvassVisits CanvassVisit[] - contactAddresses ContactAddress[] + canvassVisits CanvassVisit[] + contactAddresses ContactAddress[] @@index([locationId]) @@index([locationId, id]) @@ -821,17 +826,17 @@ enum LocationHistoryAction { } model LocationHistory { - id String @id @default(cuid()) + id String @id @default(cuid()) locationId String - location Location @relation(fields: [locationId], references: [id], onDelete: Cascade) + location Location @relation(fields: [locationId], references: [id], onDelete: Cascade) userId String? - user User? @relation("LocationHistoryUser", fields: [userId], references: [id], onDelete: SetNull) + user User? @relation("LocationHistoryUser", fields: [userId], references: [id], onDelete: SetNull) action LocationHistoryAction - field String? // Which field changed - oldValue String? @db.Text - newValue String? @db.Text - metadata Json? // Provider, confidence, etc. - createdAt DateTime @default(now()) + field String? // Which field changed + oldValue String? @db.Text + newValue String? @db.Text + metadata Json? // Provider, confidence, etc. + createdAt DateTime @default(now()) @@index([locationId]) @@index([userId]) @@ -856,45 +861,46 @@ enum RecurrenceFrequency { } model Shift { - id String @id @default(cuid()) - title String - description String? @db.Text - date DateTime @db.Date - startTime String // HH:MM format - endTime String // HH:MM format - location String? - maxVolunteers Int - currentVolunteers Int @default(0) - status ShiftStatus @default(OPEN) - isPublic Boolean @default(false) - cutId String? - cut Cut? @relation(fields: [cutId], references: [id], onDelete: SetNull) + id String @id @default(cuid()) + title String + description String? @db.Text + date DateTime @db.Date + startTime String // HH:MM format + endTime String // HH:MM format + location String? + maxVolunteers Int + currentVolunteers Int @default(0) + status ShiftStatus @default(OPEN) + kind ShiftKind @default(CANVASS) + isPublic Boolean @default(false) + cutId String? + cut Cut? @relation(fields: [cutId], references: [id], onDelete: SetNull) // Repeating shift series - seriesId String? - series ShiftSeries? @relation(fields: [seriesId], references: [id], onDelete: SetNull) - isException Boolean @default(false) + seriesId String? + series ShiftSeries? @relation(fields: [seriesId], references: [id], onDelete: SetNull) + isException Boolean @default(false) // Gancio event sync - gancioEventId Int? + gancioEventId Int? // Video briefing meeting - meetingId String? @unique - meeting Meeting? @relation("ShiftMeeting", fields: [meetingId], references: [id], onDelete: SetNull) + meetingId String? @unique + meeting Meeting? @relation("ShiftMeeting", fields: [meetingId], references: [id], onDelete: SetNull) - createdBy String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdBy String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt - signups ShiftSignup[] - canvassVisits CanvassVisit[] - canvassSessions CanvassSession[] + signups ShiftSignup[] + canvassVisits CanvassVisit[] + canvassSessions CanvassSession[] // Scheduling poll conversion convertedFromPoll SchedulingPoll? @relation("PollConvertedShift") // Meeting agenda - agenda MeetingAgenda? @relation("ShiftAgenda") + agenda MeetingAgenda? @relation("ShiftAgenda") @@index([cutId]) @@index([seriesId]) @@ -913,6 +919,14 @@ enum SignupSource { POLL_CONVERSION } +enum ShiftKind { + CANVASS + TRAINING + EVENT_STAFFING + PHONE_BANK + OTHER +} + model ShiftSignup { id String @id @default(cuid()) shiftId String @@ -937,30 +951,30 @@ model ShiftSignup { // ============================================================================ model ShiftSeries { - id String @id @default(cuid()) - title String - description String? @db.Text - startTime String // HH:MM format - endTime String // HH:MM format - location String? - maxVolunteers Int - isPublic Boolean @default(false) - cutId String? - cut Cut? @relation(fields: [cutId], references: [id], onDelete: SetNull) + id String @id @default(cuid()) + title String + description String? @db.Text + startTime String // HH:MM format + endTime String // HH:MM format + location String? + maxVolunteers Int + isPublic Boolean @default(false) + cutId String? + cut Cut? @relation(fields: [cutId], references: [id], onDelete: SetNull) // Recurrence rules - frequency RecurrenceFrequency - daysOfWeek Json? // Array of day numbers: [1,3,5] for Mon/Wed/Fri - startDate DateTime @db.Date - endDate DateTime? @db.Date + frequency RecurrenceFrequency + daysOfWeek Json? // Array of day numbers: [1,3,5] for Mon/Wed/Fri + startDate DateTime @db.Date + endDate DateTime? @db.Date // Metadata - createdBy String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdBy String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt // Relations - shifts Shift[] + shifts Shift[] @@index([cutId]) @@map("shift_series") @@ -986,23 +1000,23 @@ model Cut { category CutCategory? isPublic Boolean @default(false) isOfficial Boolean @default(false) - geojson String @db.Text // GeoJSON polygon data - bounds String? @db.Text // Bounding box JSON + geojson String @db.Text // GeoJSON polygon data + bounds String? @db.Text // Bounding box JSON showLocations Boolean @default(true) exportEnabled Boolean @default(true) assignedTo String? - filterSettings Json? // JSON filter configuration + filterSettings Json? // JSON filter configuration lastCanvassed DateTime? completionPercentage Int @default(0) - createdByUserId String? - createdByUser User? @relation("CutCreator", fields: [createdByUserId], references: [id], onDelete: SetNull) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdByUserId String? + createdByUser User? @relation("CutCreator", fields: [createdByUserId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt - shifts Shift[] - shiftSeries ShiftSeries[] - canvassSessions CanvassSession[] + shifts Shift[] + shiftSeries ShiftSeries[] + canvassSessions CanvassSession[] @@map("cuts") } @@ -1012,29 +1026,29 @@ model Cut { // ============================================================================ model MapSettings { - id String @id @default(cuid()) - latitude Decimal? @db.Decimal(10, 8) - longitude Decimal? @db.Decimal(11, 8) - zoom Int? - walkSheetTitle String? - walkSheetSubtitle String? - walkSheetFooter String? @db.Text - qrCode1Url String? - qrCode1Label String? - qrCode2Url String? - qrCode2Label String? - qrCode3Url String? - qrCode3Label String? - publicMapEnabled Boolean @default(false) - publicShowLocations Boolean @default(true) - publicShowSupportLevels Boolean @default(true) - publicShowCuts Boolean @default(true) - publicShowEvents Boolean @default(true) - publicShowAddresses Boolean @default(true) - publicShowSignInfo Boolean @default(true) - createdBy String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(cuid()) + latitude Decimal? @db.Decimal(10, 8) + longitude Decimal? @db.Decimal(11, 8) + zoom Int? + walkSheetTitle String? + walkSheetSubtitle String? + walkSheetFooter String? @db.Text + qrCode1Url String? + qrCode1Label String? + qrCode2Url String? + qrCode2Label String? + qrCode3Url String? + qrCode3Label String? + publicMapEnabled Boolean @default(false) + publicShowLocations Boolean @default(true) + publicShowSupportLevels Boolean @default(true) + publicShowCuts Boolean @default(true) + publicShowEvents Boolean @default(true) + publicShowAddresses Boolean @default(true) + publicShowSignInfo Boolean @default(true) + createdBy String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt @@map("map_settings") } @@ -1044,135 +1058,135 @@ model MapSettings { // ============================================================================ model SiteSettings { - id String @id @default(cuid()) + id String @id @default(cuid()) // Organization - organizationName String @default("Changemaker Lite") - organizationShortName String @default("CML") + organizationName String @default("Changemaker Lite") + organizationShortName String @default("CML") organizationLogoUrl String? organizationFaviconUrl String? // Admin theme - adminColorPrimary String @default("#9d4edd") - adminColorBgBase String @default("#1a1025") + adminColorPrimary String @default("#9d4edd") + adminColorBgBase String @default("#1a1025") // Public theme - publicColorPrimary String @default("#3498db") - publicColorBgBase String @default("#0d1b2a") - publicColorBgContainer String @default("#1b2838") - publicHeaderGradient String @default("linear-gradient(135deg, #005a9c 0%, #007acc 100%)") + publicColorPrimary String @default("#3498db") + publicColorBgBase String @default("#0d1b2a") + publicColorBgContainer String @default("#1b2838") + publicHeaderGradient String @default("linear-gradient(135deg, #005a9c 0%, #007acc 100%)") // Text - footerText String @default("Powered by Changemaker Lite") - loginSubtitle String @default("Admin") - homepageTagline String? @map("homepage_tagline") + footerText String @default("Powered by Changemaker Lite") + loginSubtitle String @default("Admin") + homepageTagline String? @map("homepage_tagline") // Email branding - emailFromName String @default("Changemaker Lite") + emailFromName String @default("Changemaker Lite") // SMTP configuration (overrides env vars when set; empty/0 = use env fallback) - smtpHost String @default("") - smtpPort Int @default(0) - smtpUser String @default("") - smtpPass String @default("") - smtpFromAddress String @default("") - smtpActiveProvider String @default("mailhog") // "mailhog" | "production" - emailTestMode Boolean @default(true) - testEmailRecipient String @default("") + smtpHost String @default("") + smtpPort Int @default(0) + smtpUser String @default("") + smtpPass String @default("") + smtpFromAddress String @default("") + smtpActiveProvider String @default("mailhog") // "mailhog" | "production" + emailTestMode Boolean @default(true) + testEmailRecipient String @default("") // Registration settings - enablePublicRegistration Boolean @default(true) - enableEmailVerification Boolean @default(true) - autoApproveVerifiedUsers Boolean @default(true) + enablePublicRegistration Boolean @default(true) + enableEmailVerification Boolean @default(true) + autoApproveVerifiedUsers Boolean @default(true) // Feature toggles - enableInfluence Boolean @default(true) - enableMap Boolean @default(false) - enableNewsletter Boolean @default(true) - enableLandingPages Boolean @default(true) - enableMediaFeatures Boolean @default(true) @map("enable_media_features") - enablePayments Boolean @default(false) - enableGalleryAds Boolean @default(false) @map("enable_gallery_ads") - enableChat Boolean @default(false) @map("enable_chat") - enableEvents Boolean @default(false) @map("enable_events") - enableDocsComments Boolean @default(false) @map("enable_docs_comments") - enableSms Boolean @default(false) @map("enable_sms") - enablePeople Boolean @default(false) @map("enable_people") - enableSocial Boolean @default(false) @map("enable_social") - enableMeet Boolean @default(false) @map("enable_meet") - enableMeetingPlanner Boolean @default(false) @map("enable_meeting_planner") - enableTicketedEvents Boolean @default(false) @map("enable_ticketed_events") - enableSocialCalendar Boolean @default(false) @map("enable_social_calendar") - enablePolls Boolean @default(false) @map("enable_polls") - enableAnalytics Boolean @default(false) @map("enable_analytics") - analyticsRetentionDays Int @default(90) @map("analytics_retention_days") - analyticsGeoEnabled Boolean @default(true) @map("analytics_geo_enabled") - trackAuthenticatedUsers Boolean @default(true) @map("track_authenticated_users") - enablePetitions Boolean @default(false) @map("enable_petitions") + enableInfluence Boolean @default(true) + enableMap Boolean @default(false) + enableNewsletter Boolean @default(true) + enableLandingPages Boolean @default(true) + enableMediaFeatures Boolean @default(true) @map("enable_media_features") + enablePayments Boolean @default(false) + enableGalleryAds Boolean @default(false) @map("enable_gallery_ads") + enableChat Boolean @default(false) @map("enable_chat") + enableEvents Boolean @default(false) @map("enable_events") + enableDocsComments Boolean @default(false) @map("enable_docs_comments") + enableSms Boolean @default(false) @map("enable_sms") + enablePeople Boolean @default(false) @map("enable_people") + enableSocial Boolean @default(false) @map("enable_social") + enableMeet Boolean @default(false) @map("enable_meet") + enableMeetingPlanner Boolean @default(false) @map("enable_meeting_planner") + enableTicketedEvents Boolean @default(false) @map("enable_ticketed_events") + enableSocialCalendar Boolean @default(false) @map("enable_social_calendar") + enablePolls Boolean @default(false) @map("enable_polls") + enableAnalytics Boolean @default(false) @map("enable_analytics") + analyticsRetentionDays Int @default(90) @map("analytics_retention_days") + analyticsGeoEnabled Boolean @default(true) @map("analytics_geo_enabled") + trackAuthenticatedUsers Boolean @default(true) @map("track_authenticated_users") + enablePetitions Boolean @default(false) @map("enable_petitions") enableDocsCollaboration Boolean @default(false) @map("enable_docs_collaboration") - requireEventApproval Boolean @default(true) @map("require_event_approval") - autoSyncPeopleToMap Boolean @default(false) @map("auto_sync_people_to_map") + requireEventApproval Boolean @default(true) @map("require_event_approval") + autoSyncPeopleToMap Boolean @default(false) @map("auto_sync_people_to_map") // SMS connection config (overrides env vars when non-empty) - smsTermuxApiUrl String @default("") @map("sms_termux_api_url") - smsTermuxApiKey String @default("") @map("sms_termux_api_key") // Encrypted at rest - smsTailscaleApiKey String @default("") @map("sms_tailscale_api_key") // Encrypted at rest - smsTailscaleTailnet String @default("") @map("sms_tailscale_tailnet") - smsTailscaleDeviceId String @default("") @map("sms_tailscale_device_id") - smsTailscaleDeviceName String @default("") @map("sms_tailscale_device_name") + smsTermuxApiUrl String @default("") @map("sms_termux_api_url") + smsTermuxApiKey String @default("") @map("sms_termux_api_key") // Encrypted at rest + smsTailscaleApiKey String @default("") @map("sms_tailscale_api_key") // Encrypted at rest + smsTailscaleTailnet String @default("") @map("sms_tailscale_tailnet") + smsTailscaleDeviceId String @default("") @map("sms_tailscale_device_id") + smsTailscaleDeviceName String @default("") @map("sms_tailscale_device_name") // Gitea Docs Comments (overrides env vars when set; empty = use env fallback) - giteaApiToken String @default("") // Encrypted at rest — Personal Access Token - giteaCommentsRepoOwner String @default("") - giteaCommentsRepoName String @default("docs-comments") - giteaOauthClientId String @default("") - giteaOauthClientSecret String @default("") // Encrypted at rest - giteaSetupComplete Boolean @default(false) @map("gitea_setup_complete") + giteaApiToken String @default("") // Encrypted at rest — Personal Access Token + giteaCommentsRepoOwner String @default("") + giteaCommentsRepoName String @default("docs-comments") + giteaOauthClientId String @default("") + giteaOauthClientSecret String @default("") // Encrypted at rest + giteaSetupComplete Boolean @default(false) @map("gitea_setup_complete") // Notification settings - notifyAdminShiftSignup Boolean @default(true) - notifyAdminResponseSubmitted Boolean @default(true) - notifyAdminSignRequested Boolean @default(true) - notifyAdminPetitionMilestone Boolean @default(false) @map("notify_admin_petition_milestone") - notifyAdminShiftCancellation Boolean @default(true) - notifyVolunteerSessionSummary Boolean @default(true) - notifyVolunteerCancellation Boolean @default(true) - notifyVolunteerShiftReminder Boolean @default(true) - notifyVolunteerShiftThankYou Boolean @default(true) + notifyAdminShiftSignup Boolean @default(true) + notifyAdminResponseSubmitted Boolean @default(true) + notifyAdminSignRequested Boolean @default(true) + notifyAdminPetitionMilestone Boolean @default(false) @map("notify_admin_petition_milestone") + notifyAdminShiftCancellation Boolean @default(true) + notifyVolunteerSessionSummary Boolean @default(true) + notifyVolunteerCancellation Boolean @default(true) + notifyVolunteerShiftReminder Boolean @default(true) + notifyVolunteerShiftThankYou Boolean @default(true) // SMS notification settings - smsShiftReminders Boolean @default(false) @map("sms_shift_reminders") - smsShiftReminderHours Int @default(24) @map("sms_shift_reminder_hours") - smsShiftSignupConfirm Boolean @default(false) @map("sms_shift_signup_confirm") - smsVolunteerWelcome Boolean @default(false) @map("sms_volunteer_welcome") + smsShiftReminders Boolean @default(false) @map("sms_shift_reminders") + smsShiftReminderHours Int @default(24) @map("sms_shift_reminder_hours") + smsShiftSignupConfirm Boolean @default(false) @map("sms_shift_signup_confirm") + smsVolunteerWelcome Boolean @default(false) @map("sms_volunteer_welcome") // Re-engagement settings - notifyVolunteerReengagement Boolean @default(false) @map("notify_volunteer_reengagement") - reengagementInactiveDays Int @default(30) @map("reengagement_inactive_days") - reengagementCooldownDays Int @default(30) @map("reengagement_cooldown_days") + notifyVolunteerReengagement Boolean @default(false) @map("notify_volunteer_reengagement") + reengagementInactiveDays Int @default(30) @map("reengagement_inactive_days") + reengagementCooldownDays Int @default(30) @map("reengagement_cooldown_days") // Auto-upgrade settings - enableAutoUpgrade Boolean @default(false) @map("enable_auto_upgrade") - autoUpgradeSchedule String @default("daily-3am") @map("auto_upgrade_schedule") - autoUpgradePullServices Boolean @default(false) @map("auto_upgrade_pull_services") - notifyAdminAutoUpgrade Boolean @default(true) @map("notify_admin_auto_upgrade") - useRegistryForUpgrade Boolean @default(false) @map("use_registry_for_upgrade") - giteaRegistryUrl String @default("gitea.bnkops.com/admin") @map("gitea_registry_url") + enableAutoUpgrade Boolean @default(false) @map("enable_auto_upgrade") + autoUpgradeSchedule String @default("daily-3am") @map("auto_upgrade_schedule") + autoUpgradePullServices Boolean @default(false) @map("auto_upgrade_pull_services") + notifyAdminAutoUpgrade Boolean @default(true) @map("notify_admin_auto_upgrade") + useRegistryForUpgrade Boolean @default(false) @map("use_registry_for_upgrade") + giteaRegistryUrl String @default("gitea.bnkops.com/admin") @map("gitea_registry_url") // Navigation configuration (JSON: { items: NavItem[] }) - navConfig Json? @map("nav_config") + navConfig Json? @map("nav_config") // User Provisioning (centralized user management across services) - enableUserProvisioning Boolean @default(false) @map("enable_user_provisioning") - provisionGitea Boolean @default(false) @map("provision_gitea") - provisionGiteaTiming String @default("lazy") @map("provision_gitea_timing") // 'lazy' | 'eager' - provisionVaultwarden Boolean @default(false) @map("provision_vaultwarden") - provisionVaultwardenTiming String @default("lazy") @map("provision_vaultwarden_timing") // 'lazy' | 'eager' - provisionListmonk Boolean @default(true) @map("provision_listmonk") - provisionListmonkTiming String @default("eager") @map("provision_listmonk_timing") // 'lazy' | 'eager' + enableUserProvisioning Boolean @default(false) @map("enable_user_provisioning") + provisionGitea Boolean @default(false) @map("provision_gitea") + provisionGiteaTiming String @default("lazy") @map("provision_gitea_timing") // 'lazy' | 'eager' + provisionVaultwarden Boolean @default(false) @map("provision_vaultwarden") + provisionVaultwardenTiming String @default("lazy") @map("provision_vaultwarden_timing") // 'lazy' | 'eager' + provisionListmonk Boolean @default(true) @map("provision_listmonk") + provisionListmonkTiming String @default("eager") @map("provision_listmonk_timing") // 'lazy' | 'eager' - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt @@map("site_settings") } @@ -1194,27 +1208,27 @@ enum EmailTemplateVariableType { } model EmailTemplate { - id String @id @default(cuid()) - key String @unique // e.g., "campaign-email" - name String // Display name - description String? @db.Text - category EmailTemplateCategory // INFLUENCE | MAP | SYSTEM - subjectLine String // Template with {{VAR}} support - htmlContent String @db.Text - textContent String @db.Text - isSystem Boolean @default(false) // Prevent deletion - isActive Boolean @default(true) + id String @id @default(cuid()) + key String @unique // e.g., "campaign-email" + name String // Display name + description String? @db.Text + category EmailTemplateCategory // INFLUENCE | MAP | SYSTEM + subjectLine String // Template with {{VAR}} support + htmlContent String @db.Text + textContent String @db.Text + isSystem Boolean @default(false) // Prevent deletion + isActive Boolean @default(true) - variables EmailTemplateVariable[] - versions EmailTemplateVersion[] - testLogs EmailTemplateTestLog[] + variables EmailTemplateVariable[] + versions EmailTemplateVersion[] + testLogs EmailTemplateTestLog[] - createdByUserId String - createdBy User @relation("TemplatesCreated", fields: [createdByUserId], references: [id]) - updatedByUserId String? - updatedBy User? @relation("TemplatesUpdated", fields: [updatedByUserId], references: [id]) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdByUserId String + createdBy User @relation("TemplatesCreated", fields: [createdByUserId], references: [id]) + updatedByUserId String? + updatedBy User? @relation("TemplatesUpdated", fields: [updatedByUserId], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt @@index([category]) @@index([isActive]) @@ -1222,19 +1236,19 @@ model EmailTemplate { } model EmailTemplateVariable { - id String @id @default(cuid()) - templateId String - template EmailTemplate @relation(fields: [templateId], references: [id], onDelete: Cascade) + id String @id @default(cuid()) + templateId String + template EmailTemplate @relation(fields: [templateId], references: [id], onDelete: Cascade) - key String // e.g., "USER_NAME" - label String // e.g., "User Name" - description String? @db.Text // e.g., "Name of the user sending the email" - type EmailTemplateVariableType @default(TEXT) // TEXT | VIDEO - videoId Int? // Optional FK to videos (not enforced, separate Media API DB) - isRequired Boolean @default(true) - isConditional Boolean @default(false) // Used in {{#if}} blocks - sampleValue String? @db.Text // e.g., "John Doe" - sortOrder Int @default(0) + key String // e.g., "USER_NAME" + label String // e.g., "User Name" + description String? @db.Text // e.g., "Name of the user sending the email" + type EmailTemplateVariableType @default(TEXT) // TEXT | VIDEO + videoId Int? // Optional FK to videos (not enforced, separate Media API DB) + isRequired Boolean @default(true) + isConditional Boolean @default(false) // Used in {{#if}} blocks + sampleValue String? @db.Text // e.g., "John Doe" + sortOrder Int @default(0) @@unique([templateId, key]) @@index([templateId]) @@ -1243,19 +1257,19 @@ model EmailTemplateVariable { } model EmailTemplateVersion { - id String @id @default(cuid()) - templateId String - template EmailTemplate @relation(fields: [templateId], references: [id], onDelete: Cascade) + id String @id @default(cuid()) + templateId String + template EmailTemplate @relation(fields: [templateId], references: [id], onDelete: Cascade) - versionNumber Int // Auto-increment per template - subjectLine String - htmlContent String @db.Text - textContent String @db.Text - changeNotes String? @db.Text + versionNumber Int // Auto-increment per template + subjectLine String + htmlContent String @db.Text + textContent String @db.Text + changeNotes String? @db.Text createdByUserId String - createdBy User @relation("TemplateVersionsCreated", fields: [createdByUserId], references: [id]) - createdAt DateTime @default(now()) + createdBy User @relation("TemplateVersionsCreated", fields: [createdByUserId], references: [id]) + createdAt DateTime @default(now()) @@unique([templateId, versionNumber]) @@index([templateId, createdAt(sort: Desc)]) @@ -1263,19 +1277,19 @@ model EmailTemplateVersion { } model EmailTemplateTestLog { - id String @id @default(cuid()) - templateId String - template EmailTemplate @relation(fields: [templateId], references: [id], onDelete: Cascade) + id String @id @default(cuid()) + templateId String + template EmailTemplate @relation(fields: [templateId], references: [id], onDelete: Cascade) - recipientEmail String - testData Json // Sample variable values used - success Boolean - errorMessage String? @db.Text - messageId String? // Nodemailer message ID + recipientEmail String + testData Json // Sample variable values used + success Boolean + errorMessage String? @db.Text + messageId String? // Nodemailer message ID - sentByUserId String - sentBy User @relation("TemplateTestsSent", fields: [sentByUserId], references: [id]) - sentAt DateTime @default(now()) + sentByUserId String + sentBy User @relation("TemplateTestsSent", fields: [sentByUserId], references: [id]) + sentAt DateTime @default(now()) @@index([templateId, sentAt(sort: Desc)]) @@map("email_template_test_logs") @@ -1291,47 +1305,47 @@ enum EditorMode { } enum MkdocsExportMode { - THEMED // extends main.html, content block only - STANDALONE // full HTML document, no Jinja2 inheritance + THEMED // extends main.html, content block only + STANDALONE // full HTML document, no Jinja2 inheritance } model LandingPage { - id String @id @default(cuid()) - slug String @unique - title String - description String? @db.Text - blocks Json // JSON from GrapesJS editor - htmlOutput String? @db.Text - cssOutput String? @db.Text - editorMode EditorMode @default(VISUAL) - mkdocsPath String? // Path in mkdocs/overrides/ - mkdocsStubPath String? // Path to .md stub in mkdocs/docs/ + id String @id @default(cuid()) + slug String @unique + title String + description String? @db.Text + blocks Json // JSON from GrapesJS editor + htmlOutput String? @db.Text + cssOutput String? @db.Text + editorMode EditorMode @default(VISUAL) + mkdocsPath String? // Path in mkdocs/overrides/ + mkdocsStubPath String? // Path to .md stub in mkdocs/docs/ mkdocsExportMode MkdocsExportMode @default(THEMED) mkdocsHideNav Boolean @default(true) mkdocsHideToc Boolean @default(true) mkdocsSkipExport Boolean @default(false) published Boolean @default(false) listed Boolean @default(false) - seoTitle String? - seoDescription String? @db.Text - seoImage String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + seoTitle String? + seoDescription String? @db.Text + seoImage String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt @@map("landing_pages") } model PageBlock { - id String @id @default(cuid()) - type String // hero, text, image, cta, features, testimonials, form - label String - schema Json // Block configuration schema - defaults Json // Default values - thumbnail String? - category String? - sortOrder Int @default(0) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(cuid()) + type String // hero, text, image, cta, features, testimonials, form + label String + schema Json // Block configuration schema + defaults Json // Default values + thumbnail String? + category String? + sortOrder Int @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt @@map("page_blocks") } @@ -1370,8 +1384,8 @@ model CanvassSession { startLatitude Decimal? @db.Decimal(10, 8) startLongitude Decimal? @db.Decimal(11, 8) - visits CanvassVisit[] - trackingSession TrackingSession? + visits CanvassVisit[] + trackingSession TrackingSession? @@index([userId]) @@index([cutId]) @@ -1380,25 +1394,25 @@ model CanvassSession { } model CanvassVisit { - id String @id @default(cuid()) - addressId String // Changed from locationId - address Address @relation(fields: [addressId], references: [id], onDelete: Cascade) + id String @id @default(cuid()) + addressId String // Changed from locationId + address Address @relation(fields: [addressId], references: [id], onDelete: Cascade) userId String - user User @relation("CanvassVisitor", fields: [userId], references: [id], onDelete: Cascade) + user User @relation("CanvassVisitor", fields: [userId], references: [id], onDelete: Cascade) shiftId String? - shift Shift? @relation(fields: [shiftId], references: [id], onDelete: SetNull) + shift Shift? @relation(fields: [shiftId], references: [id], onDelete: SetNull) sessionId String? session CanvassSession? @relation(fields: [sessionId], references: [id], onDelete: SetNull) outcome VisitOutcome supportLevel SupportLevel? - signRequested Boolean @default(false) + signRequested Boolean @default(false) signSize String? - notes String? @db.Text + notes String? @db.Text durationSeconds Int? - visitedAt DateTime @default(now()) + visitedAt DateTime @default(now()) - @@index([addressId]) // Changed from locationId - @@index([addressId, visitedAt(sort: Desc)]) // For distinct + orderBy queries + @@index([addressId]) // Changed from locationId + @@index([addressId, visitedAt(sort: Desc)]) // For distinct + orderBy queries @@index([userId]) @@index([shiftId]) @@index([sessionId]) @@ -1418,20 +1432,20 @@ enum TrackPointEvent { } model TrackingSession { - id String @id @default(cuid()) - userId String - user User @relation("TrackingSessions", fields: [userId], references: [id], onDelete: Cascade) - canvassSessionId String? @unique - canvassSession CanvassSession? @relation(fields: [canvassSessionId], references: [id], onDelete: SetNull) - startedAt DateTime @default(now()) - endedAt DateTime? - isActive Boolean @default(true) - totalPoints Int @default(0) - totalDistanceM Float @default(0) - lastLatitude Decimal? @db.Decimal(10, 8) - lastLongitude Decimal? @db.Decimal(11, 8) - lastRecordedAt DateTime? - trackPoints TrackPoint[] + id String @id @default(cuid()) + userId String + user User @relation("TrackingSessions", fields: [userId], references: [id], onDelete: Cascade) + canvassSessionId String? @unique + canvassSession CanvassSession? @relation(fields: [canvassSessionId], references: [id], onDelete: SetNull) + startedAt DateTime @default(now()) + endedAt DateTime? + isActive Boolean @default(true) + totalPoints Int @default(0) + totalDistanceM Float @default(0) + lastLatitude Decimal? @db.Decimal(10, 8) + lastLongitude Decimal? @db.Decimal(11, 8) + lastRecordedAt DateTime? + trackPoints TrackPoint[] @@index([userId]) @@index([isActive]) @@ -1453,6 +1467,7 @@ model TrackPoint { @@index([recordedAt]) @@map("track_points") } + // Enums enum DirectoryType { studios @@ -1712,104 +1727,104 @@ enum NotificationType { // ============================================================================ model Video { - id Int @id @default(autoincrement()) - path String @unique - filename String - producer String? - creator String? - title String? - durationSeconds Int? @map("duration_seconds") - quality String? - orientation String? - hasAudio Boolean? @default(true) @map("has_audio") - fileSize BigInt? @map("file_size") - fileHash String? @map("file_hash") - width Int? - height Int? - lastValidated DateTime? @map("last_validated") - isValid Boolean? @default(true) @map("is_valid") - thumbnailPath String? @map("thumbnail_path") - createdAt DateTime @default(now()) @map("created_at") - tags Json? - directoryType DirectoryType? @map("directory_type") + id Int @id @default(autoincrement()) + path String @unique + filename String + producer String? + creator String? + title String? + durationSeconds Int? @map("duration_seconds") + quality String? + orientation String? + hasAudio Boolean? @default(true) @map("has_audio") + fileSize BigInt? @map("file_size") + fileHash String? @map("file_hash") + width Int? + height Int? + lastValidated DateTime? @map("last_validated") + isValid Boolean? @default(true) @map("is_valid") + thumbnailPath String? @map("thumbnail_path") + createdAt DateTime @default(now()) @map("created_at") + tags Json? + directoryType DirectoryType? @map("directory_type") // Historical engagement stats - publicViewCount Int? @map("public_view_count") - publicUpvoteCount Int? @map("public_upvote_count") - publicCommentCount Int? @map("public_comment_count") - publicCompletionCount Int? @map("public_completion_count") - publicTotalWatchTime Int? @map("public_total_watch_time") - movedFromPublicAt DateTime? @map("moved_from_public_at") + publicViewCount Int? @map("public_view_count") + publicUpvoteCount Int? @map("public_upvote_count") + publicCommentCount Int? @map("public_comment_count") + publicCompletionCount Int? @map("public_completion_count") + publicTotalWatchTime Int? @map("public_total_watch_time") + movedFromPublicAt DateTime? @map("moved_from_public_at") // Name standardization tracking - originalFilename String? @map("original_filename") - originalPath String? @map("original_path") - standardizedAt DateTime? @map("standardized_at") + originalFilename String? @map("original_filename") + originalPath String? @map("original_path") + standardizedAt DateTime? @map("standardized_at") // Publishing system (replaces PublicMedia) - isPublished Boolean @default(false) @map("is_published") - publishedAt DateTime? @map("published_at") - category String? // videos|curated|compilations|playback|highlights - isShort Boolean @default(false) @map("is_short") + isPublished Boolean @default(false) @map("is_published") + publishedAt DateTime? @map("published_at") + category String? // videos|curated|compilations|playback|highlights + isShort Boolean @default(false) @map("is_short") // Moderation system - isLocked Boolean @default(false) @map("is_locked") - lockedAt DateTime? @map("locked_at") - lockedById String? @map("locked_by_id") + isLocked Boolean @default(false) @map("is_locked") + lockedAt DateTime? @map("locked_at") + lockedById String? @map("locked_by_id") // Engagement counters - viewCount Int @default(0) @map("view_count") - upvoteCount Int @default(0) @map("upvote_count") - commentCount Int @default(0) @map("comment_count") - finishCount Int @default(0) @map("finish_count") - totalWatchTime Int @default(0) @map("total_watch_time") + viewCount Int @default(0) @map("view_count") + upvoteCount Int @default(0) @map("upvote_count") + commentCount Int @default(0) @map("comment_count") + finishCount Int @default(0) @map("finish_count") + totalWatchTime Int @default(0) @map("total_watch_time") // Scheduled publishing - scheduledPublishAt DateTime? @map("scheduled_publish_at") - scheduledUnpublishAt DateTime? @map("scheduled_unpublish_at") + scheduledPublishAt DateTime? @map("scheduled_publish_at") + scheduledUnpublishAt DateTime? @map("scheduled_unpublish_at") // Enhanced analytics - uniqueViewers Int @default(0) @map("unique_viewers") - totalWatchTimeSeconds Int @default(0) @map("total_watch_time_seconds") - averageWatchTimeSeconds Decimal @default(0) @map("average_watch_time_seconds") @db.Decimal(10, 2) - completionRate Decimal @default(0) @map("completion_rate") @db.Decimal(5, 2) + uniqueViewers Int @default(0) @map("unique_viewers") + totalWatchTimeSeconds Int @default(0) @map("total_watch_time_seconds") + averageWatchTimeSeconds Decimal @default(0) @map("average_watch_time_seconds") @db.Decimal(10, 2) + completionRate Decimal @default(0) @map("completion_rate") @db.Decimal(5, 2) // Content gating - accessLevel String @default("free") @map("access_level") // free|member|premium + accessLevel String @default("free") @map("access_level") // free|member|premium // Ordering - position Int? @default(0) + position Int? @default(0) // Uploader tracking - uploaderId String? @map("uploader_id") + uploaderId String? @map("uploader_id") // Relations - uploader User? @relation("VideoUploader", fields: [uploaderId], references: [id]) - locker User? @relation("VideoLocker", fields: [lockedById], references: [id]) - upvotes Upvote[] - comments Comment[] - views View[] - reactions VideoReaction[] - finishes UserFinish[] - contentReports ContentReport[] - playlistVideos PlaylistVideo[] - publicMediaTags PublicMediaTag[] @relation("PublicMediaTags") - publicMediaPerformers PublicMediaPerformer[] @relation("PublicMediaPerformers") - recommendations VideoRecommendation[] - videoDigests VideoDigest[] - digestVideoTags DigestVideoTag[] - digestSelectedClips DigestSelectedClip[] - digestGeneratedScenes DigestGeneratedScene[] - digestOutputFolders DigestOutputFolder[] - digestCompilations DigestCompilation[] - videoSceneCuts VideoSceneCut[] - videoTagTimeline VideoTagTimeline[] - videoSegments VideoSegment[] - videoTags VideoTag[] - digestClipTags DigestClipTag[] - tagGenerationJobs TagGenerationJob[] - videoOcrResults VideoOcrResult[] - videoViews VideoView[] - videoEvents VideoEvent[] - scheduleHistory VideoScheduleHistory[] + uploader User? @relation("VideoUploader", fields: [uploaderId], references: [id]) + locker User? @relation("VideoLocker", fields: [lockedById], references: [id]) + upvotes Upvote[] + comments Comment[] + views View[] + reactions VideoReaction[] + finishes UserFinish[] + contentReports ContentReport[] + playlistVideos PlaylistVideo[] + publicMediaTags PublicMediaTag[] @relation("PublicMediaTags") + publicMediaPerformers PublicMediaPerformer[] @relation("PublicMediaPerformers") + recommendations VideoRecommendation[] + videoDigests VideoDigest[] + digestVideoTags DigestVideoTag[] + digestSelectedClips DigestSelectedClip[] + digestGeneratedScenes DigestGeneratedScene[] + digestOutputFolders DigestOutputFolder[] + digestCompilations DigestCompilation[] + videoSceneCuts VideoSceneCut[] + videoTagTimeline VideoTagTimeline[] + videoSegments VideoSegment[] + videoTags VideoTag[] + digestClipTags DigestClipTag[] + tagGenerationJobs TagGenerationJob[] + videoOcrResults VideoOcrResult[] + videoViews VideoView[] + videoEvents VideoEvent[] + scheduleHistory VideoScheduleHistory[] @@index([orientation], map: "idx_orientation") @@index([producer], map: "idx_producer") @@ -1825,41 +1840,41 @@ model Video { } model Compilation { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) filename String path String? - durationSeconds Int? @map("duration_seconds") - videoIds Json? @map("video_ids") + durationSeconds Int? @map("duration_seconds") + videoIds Json? @map("video_ids") settings Json? - createdAt DateTime @default(now()) @map("created_at") + createdAt DateTime @default(now()) @map("created_at") @@map("compilations") } model Job { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) type String - status JobStatus? @default(pending) - progress Int? @default(0) + status JobStatus? @default(pending) + progress Int? @default(0) log String? params Json? - startedAt DateTime? @map("started_at") - completedAt DateTime? @map("completed_at") - createdAt DateTime @default(now()) @map("created_at") + startedAt DateTime? @map("started_at") + completedAt DateTime? @map("completed_at") + createdAt DateTime @default(now()) @map("created_at") // Queue management resourceCategory ResourceCategory? @default(cpu) @map("resource_category") - vramRequired Int? @default(0) @map("vram_required") - queuePosition Int? @map("queue_position") - waitingReason String? @map("waiting_reason") - priority Int? @default(5) + vramRequired Int? @default(0) @map("vram_required") + queuePosition Int? @map("queue_position") + waitingReason String? @map("waiting_reason") + priority Int? @default(5) // Pipeline integration - pipelineId Int? @map("pipeline_id") - pipelineStepId Int? @map("pipeline_step_id") + pipelineId Int? @map("pipeline_id") + pipelineStepId Int? @map("pipeline_step_id") // Relations - pipeline Pipeline? @relation(fields: [pipelineId], references: [id]) - pipelineStep PipelineStep? @relation(fields: [pipelineStepId], references: [id]) - videoDigest VideoDigest? + pipeline Pipeline? @relation(fields: [pipelineId], references: [id]) + pipelineStep PipelineStep? @relation(fields: [pipelineStepId], references: [id]) + videoDigest VideoDigest? tagGenerationJobs TagGenerationJob[] @@index([status, priority, createdAt], map: "idx_jobs_queue") @@ -1912,31 +1927,31 @@ model Job { // } model Session { - id String @id - createdAt DateTime @default(now()) @map("created_at") - lastSeenAt DateTime? @map("last_seen_at") + id String @id + createdAt DateTime @default(now()) @map("created_at") + lastSeenAt DateTime? @map("last_seen_at") // Device tracking - ipAddress String? @map("ip_address") - userAgent String? @map("user_agent") - deviceType String? @map("device_type") - browser String? - os String? + ipAddress String? @map("ip_address") + userAgent String? @map("user_agent") + deviceType String? @map("device_type") + browser String? + os String? // Geography - country String? - countryName String? @map("country_name") - region String? - city String? - timezone String? - latitude Float? @db.Real - longitude Float? @db.Real + country String? + countryName String? @map("country_name") + region String? + city String? + timezone String? + latitude Float? @db.Real + longitude Float? @db.Real // User correlation - userId String? @map("user_id") + userId String? @map("user_id") // Analytics - firstSeenAt DateTime? @map("first_seen_at") - visitCount Int? @default(1) @map("visit_count") + firstSeenAt DateTime? @map("first_seen_at") + visitCount Int? @default(1) @map("visit_count") // Relations - user User? @relation("SessionUser", fields: [userId], references: [id]) + user User? @relation("SessionUser", fields: [userId], references: [id]) upvotes Upvote[] comments Comment[] views View[] @@ -1948,9 +1963,9 @@ model Session { userFinishes UserFinish[] // Photo gallery relations - photoUpvotes PhotoUpvote[] @relation("SessionPhotoUpvotes") - photoComments PhotoComment[] @relation("SessionPhotoComments") - photoReactions PhotoReaction[] @relation("SessionPhotoReactions") + photoUpvotes PhotoUpvote[] @relation("SessionPhotoUpvotes") + photoComments PhotoComment[] @relation("SessionPhotoComments") + photoReactions PhotoReaction[] @relation("SessionPhotoReactions") @@index([userId], map: "idx_sessions_user_id") @@index([country], map: "idx_sessions_country") @@ -1964,8 +1979,8 @@ model Upvote { createdAt DateTime @default(now()) @map("created_at") // Relations - media Video @relation(fields: [mediaId], references: [id]) - session Session @relation(fields: [sessionId], references: [id]) + media Video @relation(fields: [mediaId], references: [id]) + session Session @relation(fields: [sessionId], references: [id]) @@index([mediaId, sessionId], map: "idx_upvotes_unique") @@index([mediaId], map: "idx_upvotes_media") @@ -1991,10 +2006,10 @@ model Comment { moderationNotes String? @map("moderation_notes") // Relations - media Video @relation(fields: [mediaId], references: [id]) - session Session @relation(fields: [sessionId], references: [id]) - user User? @relation("CommentUser", fields: [userId], references: [id]) - moderation CommentModeration? + media Video @relation(fields: [mediaId], references: [id]) + session Session @relation(fields: [sessionId], references: [id]) + user User? @relation("CommentUser", fields: [userId], references: [id]) + moderation CommentModeration? watchPartyMessages WatchPartyChatMessage[] @@index([mediaId], map: "idx_comments_media") @@ -2014,8 +2029,8 @@ model View { createdAt DateTime @default(now()) @map("created_at") // Relations - media Video @relation(fields: [mediaId], references: [id]) - session Session @relation(fields: [sessionId], references: [id]) + media Video @relation(fields: [mediaId], references: [id]) + session Session @relation(fields: [sessionId], references: [id]) @@index([mediaId, sessionId], map: "idx_views_unique") @@index([mediaId], map: "idx_views_media") @@ -2035,7 +2050,7 @@ model AuthToken { createdAt DateTime @default(now()) @map("created_at") // Relations - user User @relation("AuthTokenUser", fields: [userId], references: [id]) + user User @relation("AuthTokenUser", fields: [userId], references: [id]) @@index([token], map: "idx_auth_tokens_token") @@index([userId], map: "idx_auth_tokens_user") @@ -2051,8 +2066,8 @@ model SessionBan { expiresAt DateTime? @map("expires_at") // Relations - session Session @relation(fields: [sessionId], references: [id]) - banner User? @relation("SessionBanner", fields: [bannedBy], references: [id]) + session Session @relation(fields: [sessionId], references: [id]) + banner User? @relation("SessionBanner", fields: [bannedBy], references: [id]) @@index([sessionId], map: "idx_session_bans_session") @@map("session_bans") @@ -2067,8 +2082,8 @@ model CommentModeration { reason String? // Relations - comment Comment @relation(fields: [commentId], references: [id]) - moderator User? @relation("CommentModerator", fields: [moderatedBy], references: [id]) + comment Comment @relation(fields: [commentId], references: [id]) + moderator User? @relation("CommentModerator", fields: [moderatedBy], references: [id]) @@index([commentId], map: "idx_comment_moderation_comment") @@index([status], map: "idx_comment_moderation_status") @@ -2083,7 +2098,7 @@ model EmailVerificationToken { createdAt DateTime @default(now()) @map("created_at") // Relations - user User @relation("EmailVerificationTokens", fields: [userId], references: [id]) + user User @relation("EmailVerificationTokens", fields: [userId], references: [id]) @@index([token], map: "idx_email_verification_tokens_token") @@index([userId], map: "idx_email_verification_tokens_user") @@ -2099,7 +2114,7 @@ model PasswordResetToken { usedAt DateTime? @map("used_at") // Relations - user User @relation("PasswordResetTokens", fields: [userId], references: [id]) + user User @relation("PasswordResetTokens", fields: [userId], references: [id]) @@index([token], map: "idx_password_reset_tokens_token") @@index([userId], map: "idx_password_reset_tokens_user") @@ -2115,7 +2130,7 @@ model EmailChangeToken { createdAt DateTime @default(now()) @map("created_at") // Relations - user User @relation("EmailChangeTokens", fields: [userId], references: [id]) + user User @relation("EmailChangeTokens", fields: [userId], references: [id]) @@index([token], map: "idx_email_change_tokens_token") @@index([userId], map: "idx_email_change_tokens_user") @@ -2135,7 +2150,7 @@ model UserAchievement { notified Boolean? @default(false) // Relations - user User @relation("UserAchievements", fields: [userId], references: [id]) + user User @relation("UserAchievements", fields: [userId], references: [id]) @@unique([userId, achievementId], map: "idx_user_achievements_unique") @@index([userId], map: "idx_user_achievements_user") @@ -2160,22 +2175,22 @@ model UserStats { updatedAt DateTime? @map("updated_at") // Relations - user User @relation("UserStats", fields: [userId], references: [id]) + user User @relation("UserStats", fields: [userId], references: [id]) @@map("user_stats") } model UserFinish { - id Int @id @default(autoincrement()) - userId String @map("user_id") - mediaId Int? @map("media_id") - sessionId String? @map("session_id") - createdAt DateTime @map("created_at") + id Int @id @default(autoincrement()) + userId String @map("user_id") + mediaId Int? @map("media_id") + sessionId String? @map("session_id") + createdAt DateTime @map("created_at") // Relations - user User @relation("UserFinishes", fields: [userId], references: [id]) - media Video? @relation(fields: [mediaId], references: [id]) - session Session? @relation(fields: [sessionId], references: [id]) + user User @relation("UserFinishes", fields: [userId], references: [id]) + media Video? @relation(fields: [mediaId], references: [id]) + session Session? @relation(fields: [sessionId], references: [id]) @@index([userId], map: "idx_user_finishes_user") @@index([createdAt], map: "idx_user_finishes_date") @@ -2183,16 +2198,16 @@ model UserFinish { } model VideoReaction { - id Int @id @default(autoincrement()) - userId String @map("user_id") - mediaId Int @map("media_id") + id Int @id @default(autoincrement()) + userId String @map("user_id") + mediaId Int @map("media_id") reactionType ReactionType @map("reaction_type") - videoTimestamp Int @map("video_timestamp") - createdAt DateTime @default(now()) @map("created_at") + videoTimestamp Int @map("video_timestamp") + createdAt DateTime @default(now()) @map("created_at") // Relations - user User @relation("VideoReactions", fields: [userId], references: [id]) - media Video @relation(fields: [mediaId], references: [id]) + user User @relation("VideoReactions", fields: [userId], references: [id]) + media Video @relation(fields: [mediaId], references: [id]) @@index([userId, mediaId, reactionType], map: "idx_video_reactions_user_media_type") @@index([mediaId, videoTimestamp], map: "idx_video_reactions_media_timestamp") @@ -2207,7 +2222,7 @@ model HighlightCooldown { lastGeneratedAt DateTime @map("last_generated_at") // Relations - user User @relation("HighlightCooldowns", fields: [userId], references: [id]) + user User @relation("HighlightCooldowns", fields: [userId], references: [id]) @@index([userId], map: "idx_highlight_cooldowns_user") @@map("highlight_cooldowns") @@ -2223,7 +2238,7 @@ model UserDailyActivity { createdAt DateTime? @map("created_at") // Relations - user User @relation("UserDailyActivity", fields: [userId], references: [id]) + user User @relation("UserDailyActivity", fields: [userId], references: [id]) @@index([userId, activityDate], map: "idx_user_daily_activity_unique") @@map("user_daily_activity") @@ -2236,7 +2251,7 @@ model ChatThreadReadStatus { lastSeenAt DateTime @map("last_seen_at") // Relations - user User @relation("ChatThreadReadStatus", fields: [userId], references: [id]) + user User @relation("ChatThreadReadStatus", fields: [userId], references: [id]) @@index([userId, mediaId], map: "idx_chat_thread_read_unique") @@index([userId], map: "idx_chat_thread_read_user") @@ -2267,14 +2282,14 @@ model RateLimit { } model ModerationWordList { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) level WordFilterLevel word String - createdAt DateTime @default(now()) @map("created_at") - createdBy String? @map("created_by") + createdAt DateTime @default(now()) @map("created_at") + createdBy String? @map("created_by") // Relations - creator User? @relation("ModerationWordListCreator", fields: [createdBy], references: [id]) + creator User? @relation("ModerationWordListCreator", fields: [createdBy], references: [id]) @@index([level], map: "idx_moderation_word_lists_level") @@index([word], map: "idx_moderation_word_lists_word") @@ -2295,10 +2310,10 @@ model ContentReport { createdAt DateTime @default(now()) @map("created_at") // Relations - media Video @relation(fields: [mediaId], references: [id]) - session Session? @relation(fields: [sessionId], references: [id]) - user User? @relation("ContentReportUser", fields: [userId], references: [id]) - resolver User? @relation("ContentReportResolver", fields: [resolvedBy], references: [id]) + media Video @relation(fields: [mediaId], references: [id]) + session Session? @relation(fields: [sessionId], references: [id]) + user User? @relation("ContentReportUser", fields: [userId], references: [id]) + resolver User? @relation("ContentReportResolver", fields: [resolvedBy], references: [id]) @@index([mediaId], map: "idx_content_reports_media") @@index([status], map: "idx_content_reports_status") @@ -2326,10 +2341,10 @@ model Playlist { updatedAt DateTime? @map("updated_at") // Relations - user User @relation("UserPlaylists", fields: [userId], references: [id]) - videos PlaylistVideo[] - featured FeaturedPlaylist? - views PlaylistView[] + user User @relation("UserPlaylists", fields: [userId], references: [id]) + videos PlaylistVideo[] + featured FeaturedPlaylist? + views PlaylistView[] @@unique([userId, name], map: "idx_playlists_user_name") @@index([userId], map: "idx_playlists_user") @@ -2346,8 +2361,8 @@ model PlaylistVideo { addedAt DateTime @default(now()) @map("added_at") // Relations - playlist Playlist @relation(fields: [playlistId], references: [id], onDelete: Cascade) - media Video @relation(fields: [mediaId], references: [id]) + playlist Playlist @relation(fields: [playlistId], references: [id], onDelete: Cascade) + media Video @relation(fields: [mediaId], references: [id]) @@index([playlistId], map: "idx_playlist_videos_playlist") @@index([mediaId], map: "idx_playlist_videos_media") @@ -2363,8 +2378,8 @@ model FeaturedPlaylist { featuredAt DateTime? @map("featured_at") // Relations - playlist Playlist @relation(fields: [playlistId], references: [id], onDelete: Cascade) - featurer User? @relation("FeaturedPlaylistFeaturer", fields: [featuredBy], references: [id]) + playlist Playlist @relation(fields: [playlistId], references: [id], onDelete: Cascade) + featurer User? @relation("FeaturedPlaylistFeaturer", fields: [featuredBy], references: [id]) @@index([position], map: "idx_featured_playlists_position") @@map("featured_playlists") @@ -2377,8 +2392,8 @@ model PlaylistView { createdAt DateTime @default(now()) @map("created_at") // Relations - playlist Playlist @relation(fields: [playlistId], references: [id], onDelete: Cascade) - session Session @relation(fields: [sessionId], references: [id]) + playlist Playlist @relation(fields: [playlistId], references: [id], onDelete: Cascade) + session Session @relation(fields: [sessionId], references: [id]) @@index([playlistId], map: "idx_playlist_views_playlist") @@index([playlistId, sessionId], map: "idx_playlist_views_unique") @@ -2390,35 +2405,35 @@ model PlaylistView { // ============================================================================ model Ad { - id Int @id @default(autoincrement()) - type String - variant String? - imagePath String? @map("image_path") - linkUrl String? @map("link_url") - title String? - subtitle String? @db.Text - ctaText String? @map("cta_text") - ctaStyle String? @default("primary") @map("cta_style") - bgColor String? @map("bg_color") - iconEmoji String? @map("icon_emoji") - isSystemAd Boolean @default(false) @map("is_system_ad") - frequency Int @default(6) - visibility String @default("everyone") - isActive Boolean? @default(true) @map("is_active") - position Int? @default(0) - impressionCount Int? @default(0) @map("impression_count") - clickCount Int? @default(0) @map("click_count") - startDate DateTime? @map("start_date") - endDate DateTime? @map("end_date") - placements Json? @default("[]") - productId String? @unique @map("product_id") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime? @map("updated_at") + id Int @id @default(autoincrement()) + type String + variant String? + imagePath String? @map("image_path") + linkUrl String? @map("link_url") + title String? + subtitle String? @db.Text + ctaText String? @map("cta_text") + ctaStyle String? @default("primary") @map("cta_style") + bgColor String? @map("bg_color") + iconEmoji String? @map("icon_emoji") + isSystemAd Boolean @default(false) @map("is_system_ad") + frequency Int @default(6) + visibility String @default("everyone") + isActive Boolean? @default(true) @map("is_active") + position Int? @default(0) + impressionCount Int? @default(0) @map("impression_count") + clickCount Int? @default(0) @map("click_count") + startDate DateTime? @map("start_date") + endDate DateTime? @map("end_date") + placements Json? @default("[]") + productId String? @unique @map("product_id") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime? @map("updated_at") // Relations - impressions AdImpression[] - clicks AdClick[] - product Product? @relation(fields: [productId], references: [id], onDelete: SetNull) + impressions AdImpression[] + clicks AdClick[] + product Product? @relation(fields: [productId], references: [id], onDelete: SetNull) @@index([type], map: "idx_ads_type") @@index([isActive], map: "idx_ads_is_active") @@ -2434,9 +2449,9 @@ model AdImpression { createdAt DateTime @default(now()) @map("created_at") // Relations - ad Ad @relation(fields: [adId], references: [id], onDelete: Cascade) - session Session? @relation(fields: [sessionId], references: [id]) - user User? @relation("AdImpressionUser", fields: [userId], references: [id]) + ad Ad @relation(fields: [adId], references: [id], onDelete: Cascade) + session Session? @relation(fields: [sessionId], references: [id]) + user User? @relation("AdImpressionUser", fields: [userId], references: [id]) @@index([adId], map: "idx_ad_impressions_ad") @@index([sessionId], map: "idx_ad_impressions_session") @@ -2452,9 +2467,9 @@ model AdClick { createdAt DateTime @default(now()) @map("created_at") // Relations - ad Ad @relation(fields: [adId], references: [id], onDelete: Cascade) - session Session? @relation(fields: [sessionId], references: [id]) - user User? @relation("AdClickUser", fields: [userId], references: [id]) + ad Ad @relation(fields: [adId], references: [id], onDelete: Cascade) + session Session? @relation(fields: [sessionId], references: [id]) + user User? @relation("AdClickUser", fields: [userId], references: [id]) @@index([adId], map: "idx_ad_clicks_ad") @@index([sessionId], map: "idx_ad_clicks_session") @@ -2475,8 +2490,8 @@ model Friendship { acceptedAt DateTime? @map("accepted_at") // Relations - user User @relation("UserFriendships", fields: [userId], references: [id]) - friend User @relation("UserFriends", fields: [friendId], references: [id]) + user User @relation("UserFriendships", fields: [userId], references: [id]) + friend User @relation("UserFriends", fields: [friendId], references: [id]) @@index([userId, friendId], map: "idx_friendships_user_friend") @@index([userId], map: "idx_friendships_user") @@ -2492,8 +2507,8 @@ model UserBlock { createdAt DateTime @default(now()) @map("created_at") // Relations - user User @relation("UserBlocks", fields: [userId], references: [id]) - blockedUser User @relation("UserBlockedBy", fields: [blockedUserId], references: [id]) + user User @relation("UserBlocks", fields: [userId], references: [id]) + blockedUser User @relation("UserBlockedBy", fields: [blockedUserId], references: [id]) @@index([userId, blockedUserId], map: "idx_user_blocks_unique") @@index([userId], map: "idx_user_blocks_user") @@ -2508,8 +2523,8 @@ model Poke { createdAt DateTime @default(now()) @map("created_at") // Relations - from User @relation("PokesSent", fields: [fromUserId], references: [id]) - to User @relation("PokesReceived", fields: [toUserId], references: [id]) + from User @relation("PokesSent", fields: [fromUserId], references: [id]) + to User @relation("PokesReceived", fields: [toUserId], references: [id]) @@index([toUserId], map: "idx_pokes_to_user") @@index([fromUserId], map: "idx_pokes_from_user") @@ -2526,9 +2541,9 @@ model VideoRecommendation { createdAt DateTime @default(now()) @map("created_at") // Relations - from User @relation("RecommendationsSent", fields: [fromUserId], references: [id]) - to User @relation("RecommendationsReceived", fields: [toUserId], references: [id]) - media Video @relation(fields: [mediaId], references: [id]) + from User @relation("RecommendationsSent", fields: [fromUserId], references: [id]) + to User @relation("RecommendationsReceived", fields: [toUserId], references: [id]) + media Video @relation(fields: [mediaId], references: [id]) @@index([toUserId], map: "idx_video_recommendations_to_user") @@index([fromUserId], map: "idx_video_recommendations_from_user") @@ -2545,7 +2560,7 @@ model UserPresence { lastVideoChangeAt DateTime? @map("last_video_change_at") // Relations - user User @relation("UserPresence", fields: [userId], references: [id]) + user User @relation("UserPresence", fields: [userId], references: [id]) @@index([isOnline], map: "idx_user_presence_online") @@index([userId], map: "idx_user_presence_user") @@ -2561,7 +2576,7 @@ model UserGalleryImage { uploadedAt DateTime @default(now()) @map("uploaded_at") // Relations - user User @relation("UserGalleryImages", fields: [userId], references: [id]) + user User @relation("UserGalleryImages", fields: [userId], references: [id]) @@index([userId], map: "idx_user_gallery_user") @@index([userId, position], map: "idx_user_gallery_position") @@ -2578,7 +2593,7 @@ model UserSocialLink { createdAt DateTime @default(now()) @map("created_at") // Relations - user User @relation("UserSocialLinks", fields: [userId], references: [id]) + user User @relation("UserSocialLinks", fields: [userId], references: [id]) @@index([userId], map: "idx_user_social_links_user") @@index([userId, position], map: "idx_user_social_links_position") @@ -2601,7 +2616,7 @@ model PrivacySettings { updatedAt DateTime? @map("updated_at") // Relations - user User @relation("PrivacySettings", fields: [userId], references: [id], onDelete: Cascade) + user User @relation("PrivacySettings", fields: [userId], references: [id], onDelete: Cascade) @@index([userId], map: "idx_privacy_settings_user") @@map("privacy_settings") @@ -2614,8 +2629,8 @@ model CloseFriend { addedAt DateTime @default(now()) @map("added_at") // Relations - user User @relation("CloseFriends", fields: [userId], references: [id], onDelete: Cascade) - closeFriend User @relation("CloseFriendOf", fields: [closeFriendId], references: [id], onDelete: Cascade) + user User @relation("CloseFriends", fields: [userId], references: [id], onDelete: Cascade) + closeFriend User @relation("CloseFriendOf", fields: [closeFriendId], references: [id], onDelete: Cascade) @@unique([userId, closeFriendId], map: "idx_close_friends_unique") @@index([userId], map: "idx_close_friends_user") @@ -2633,13 +2648,13 @@ enum SocialGroupType { } model SocialGroup { - id String @id @default(cuid()) + id String @id @default(cuid()) name String type SocialGroupType - referenceId String? @map("reference_id") - meetingId String? @unique @map("meeting_id") - meeting Meeting? @relation("GroupMeeting", fields: [meetingId], references: [id], onDelete: SetNull) - createdAt DateTime @default(now()) @map("created_at") + referenceId String? @map("reference_id") + meetingId String? @unique @map("meeting_id") + meeting Meeting? @relation("GroupMeeting", fields: [meetingId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) @map("created_at") members SocialGroupMember[] @@unique([type, referenceId], map: "idx_social_groups_type_ref") @@ -2683,10 +2698,10 @@ model UserUpload { createdAt DateTime @default(now()) @map("created_at") // Relations - user User @relation("UserUploads", fields: [userId], references: [id]) - reviewer User? @relation("UserUploadReviewer", fields: [reviewedBy], references: [id]) - uploadInvite UploadInvite? @relation(fields: [uploadInviteId], references: [id]) - suggestedTags UserUploadSuggestedTag[] + user User @relation("UserUploads", fields: [userId], references: [id]) + reviewer User? @relation("UserUploadReviewer", fields: [reviewedBy], references: [id]) + uploadInvite UploadInvite? @relation(fields: [uploadInviteId], references: [id]) + suggestedTags UserUploadSuggestedTag[] @@index([userId], map: "idx_user_uploads_user") @@index([status], map: "idx_user_uploads_status") @@ -2707,8 +2722,8 @@ model UploadInvite { createdAt DateTime @default(now()) @map("created_at") // Relations - creator User @relation("UploadInviteCreator", fields: [createdBy], references: [id]) - uploads UserUpload[] + creator User @relation("UploadInviteCreator", fields: [createdBy], references: [id]) + uploads UserUpload[] @@index([code], map: "idx_upload_invites_code") @@index([status], map: "idx_upload_invites_status") @@ -2727,7 +2742,7 @@ model TagCategory { createdAt DateTime @default(now()) @map("created_at") // Relations - tags Tag[] + tags Tag[] @@index([displayOrder], map: "idx_tag_categories_display_order") @@map("tag_categories") @@ -2741,10 +2756,10 @@ model Tag { createdAt DateTime @default(now()) @map("created_at") // Relations - category TagCategory @relation(fields: [categoryId], references: [id]) - publicMedia PublicMediaTag[] + category TagCategory @relation(fields: [categoryId], references: [id]) + publicMedia PublicMediaTag[] userUploadSuggestions UserUploadSuggestedTag[] - userPreferences UserTagPreference[] + userPreferences UserTagPreference[] @@index([categoryId], map: "idx_tags_category") @@index([displayOrder], map: "idx_tags_display_order") @@ -2759,8 +2774,8 @@ model PublicMediaTag { addedAt DateTime @default(now()) @map("added_at") // Relations - media Video @relation("PublicMediaTags", fields: [mediaId], references: [id]) - tag Tag @relation(fields: [tagId], references: [id]) + media Video @relation("PublicMediaTags", fields: [mediaId], references: [id]) + tag Tag @relation(fields: [tagId], references: [id]) @@index([mediaId], map: "idx_public_media_tags_media") @@index([tagId], map: "idx_public_media_tags_tag") @@ -2775,8 +2790,8 @@ model UserUploadSuggestedTag { suggestedAt DateTime @default(now()) @map("suggested_at") // Relations - upload UserUpload @relation(fields: [uploadId], references: [id]) - tag Tag @relation(fields: [tagId], references: [id]) + upload UserUpload @relation(fields: [uploadId], references: [id]) + tag Tag @relation(fields: [tagId], references: [id]) @@index([uploadId], map: "idx_user_upload_suggested_tags_upload") @@index([tagId], map: "idx_user_upload_suggested_tags_tag") @@ -2791,8 +2806,8 @@ model UserTagPreference { savedAt DateTime @default(now()) @map("saved_at") // Relations - user User @relation("UserTagPreferences", fields: [userId], references: [id]) - tag Tag @relation(fields: [tagId], references: [id]) + user User @relation("UserTagPreferences", fields: [userId], references: [id]) + tag Tag @relation(fields: [tagId], references: [id]) @@index([userId], map: "idx_user_tag_preferences_user") @@index([tagId], map: "idx_user_tag_preferences_tag") @@ -2807,8 +2822,8 @@ model PublicMediaPerformer { addedAt DateTime @default(now()) @map("added_at") // Relations - media Video @relation("PublicMediaPerformers", fields: [mediaId], references: [id]) - performer Creator @relation(fields: [performerId], references: [id]) + media Video @relation("PublicMediaPerformers", fields: [mediaId], references: [id]) + performer Creator @relation(fields: [performerId], references: [id]) @@unique([mediaId, performerId], map: "idx_public_media_performers_unique") @@index([mediaId], map: "idx_public_media_performers_media") @@ -2821,34 +2836,34 @@ model PublicMediaPerformer { // ============================================================================ model VideoDigest { - id Int @id @default(autoincrement()) - videoId Int @map("video_id") - jobId Int? @unique @map("job_id") - status DigestStatus @default(pending) - progress Int? @default(0) - frameCount Int? @map("frame_count") + id Int @id @default(autoincrement()) + videoId Int @map("video_id") + jobId Int? @unique @map("job_id") + status DigestStatus @default(pending) + progress Int? @default(0) + frameCount Int? @map("frame_count") config Json? - frameAnalyses Json? @map("frame_analyses") + frameAnalyses Json? @map("frame_analyses") transcript Json? tags Json? - suggestedClips Json? @map("suggested_clips") - adCutSpec Json? @map("ad_cut_spec") - stageResults Json? @map("stage_results") - createdAt DateTime @default(now()) @map("created_at") - startedAt DateTime? @map("started_at") - completedAt DateTime? @map("completed_at") + suggestedClips Json? @map("suggested_clips") + adCutSpec Json? @map("ad_cut_spec") + stageResults Json? @map("stage_results") + createdAt DateTime @default(now()) @map("created_at") + startedAt DateTime? @map("started_at") + completedAt DateTime? @map("completed_at") error String? // Relations - video Video @relation(fields: [videoId], references: [id]) - job Job? @relation(fields: [jobId], references: [id]) + video Video @relation(fields: [videoId], references: [id]) + job Job? @relation(fields: [jobId], references: [id]) digestVideoTags DigestVideoTag[] - selectedClips DigestSelectedClip[] - generatedClips DigestGeneratedClip[] - outputFolders DigestOutputFolder[] - compilations DigestCompilation[] + selectedClips DigestSelectedClip[] + generatedClips DigestGeneratedClip[] + outputFolders DigestOutputFolder[] + compilations DigestCompilation[] generatedScenes DigestGeneratedScene[] - suggestedTags DigestSuggestedTag[] + suggestedTags DigestSuggestedTag[] @@index([videoId], map: "idx_video_digests_video") @@index([status], map: "idx_video_digests_status") @@ -2868,8 +2883,8 @@ model DigestVideoTag { createdAt DateTime @default(now()) @map("created_at") // Relations - digest VideoDigest @relation(fields: [digestId], references: [id]) - video Video @relation(fields: [videoId], references: [id]) + digest VideoDigest @relation(fields: [digestId], references: [id]) + video Video @relation(fields: [videoId], references: [id]) @@index([digestId], map: "idx_digest_video_tags_digest") @@index([videoId], map: "idx_digest_video_tags_video") @@ -2880,29 +2895,29 @@ model DigestVideoTag { } model DigestSelectedClip { - id Int @id @default(autoincrement()) - digestId Int @map("digest_id") - videoId Int @map("video_id") - clipType ClipType @map("clip_type") - startTime Int @map("start_time") - endTime Int @map("end_time") - duration Int - reason String? - interestScore Int? @map("interest_score") - position String? - transcriptHint String? @map("transcript_hint") - tags Json? - source ClipSource @default(machine) - isIncluded Int @default(1) @map("is_included") - isHook Int @default(0) @map("is_hook") - sequenceOrder Int @default(0) @map("sequence_order") - hookSourceClipId Int? @map("hook_source_clip_id") - createdAt DateTime @default(now()) @map("created_at") + id Int @id @default(autoincrement()) + digestId Int @map("digest_id") + videoId Int @map("video_id") + clipType ClipType @map("clip_type") + startTime Int @map("start_time") + endTime Int @map("end_time") + duration Int + reason String? + interestScore Int? @map("interest_score") + position String? + transcriptHint String? @map("transcript_hint") + tags Json? + source ClipSource @default(machine) + isIncluded Int @default(1) @map("is_included") + isHook Int @default(0) @map("is_hook") + sequenceOrder Int @default(0) @map("sequence_order") + hookSourceClipId Int? @map("hook_source_clip_id") + createdAt DateTime @default(now()) @map("created_at") // Relations - digest VideoDigest @relation(fields: [digestId], references: [id]) - video Video @relation(fields: [videoId], references: [id]) - generatedClips DigestGeneratedClip[] + digest VideoDigest @relation(fields: [digestId], references: [id]) + video Video @relation(fields: [videoId], references: [id]) + generatedClips DigestGeneratedClip[] @@index([digestId], map: "idx_digest_selected_clips_digest") @@index([videoId], map: "idx_digest_selected_clips_video") @@ -2915,23 +2930,23 @@ model DigestSelectedClip { } model DigestGeneratedClip { - id Int @id @default(autoincrement()) - selectedClipId Int @map("selected_clip_id") - digestId Int @map("digest_id") - folderId Int? @map("folder_id") - clipPath String? @map("clip_path") - gifPath String? @map("gif_path") - status ClipStatus @default(pending) - error String? - createdAt DateTime @default(now()) @map("created_at") - completedAt DateTime? @map("completed_at") - publishedToPublicMediaId Int? @map("published_to_public_media_id") - publishedAt DateTime? @map("published_at") + id Int @id @default(autoincrement()) + selectedClipId Int @map("selected_clip_id") + digestId Int @map("digest_id") + folderId Int? @map("folder_id") + clipPath String? @map("clip_path") + gifPath String? @map("gif_path") + status ClipStatus @default(pending) + error String? + createdAt DateTime @default(now()) @map("created_at") + completedAt DateTime? @map("completed_at") + publishedToPublicMediaId Int? @map("published_to_public_media_id") + publishedAt DateTime? @map("published_at") // Relations - selectedClip DigestSelectedClip @relation(fields: [selectedClipId], references: [id]) - digest VideoDigest @relation(fields: [digestId], references: [id]) - folder DigestOutputFolder? @relation(fields: [folderId], references: [id]) + selectedClip DigestSelectedClip @relation(fields: [selectedClipId], references: [id]) + digest VideoDigest @relation(fields: [digestId], references: [id]) + folder DigestOutputFolder? @relation(fields: [folderId], references: [id]) @@index([selectedClipId], map: "idx_digest_generated_clips_selected") @@index([digestId], map: "idx_digest_generated_clips_digest") @@ -2941,29 +2956,29 @@ model DigestGeneratedClip { } model DigestCompilation { - id Int @id @default(autoincrement()) - digestId Int @map("digest_id") - videoId Int @map("video_id") - folderId Int? @map("folder_id") - filename String - name String? - path String - durationSeconds Int? @map("duration_seconds") - orientation String? - status String @default("pending") - error String? - hasCaptions Int @default(0) @map("has_captions") - captionStyle Json? @map("caption_style") - closingAdPath String? @map("closing_ad_path") - closingAdDuration Int? @map("closing_ad_duration") - tags Json? - createdAt DateTime @default(now()) @map("created_at") - completedAt DateTime? @map("completed_at") + id Int @id @default(autoincrement()) + digestId Int @map("digest_id") + videoId Int @map("video_id") + folderId Int? @map("folder_id") + filename String + name String? + path String + durationSeconds Int? @map("duration_seconds") + orientation String? + status String @default("pending") + error String? + hasCaptions Int @default(0) @map("has_captions") + captionStyle Json? @map("caption_style") + closingAdPath String? @map("closing_ad_path") + closingAdDuration Int? @map("closing_ad_duration") + tags Json? + createdAt DateTime @default(now()) @map("created_at") + completedAt DateTime? @map("completed_at") // Relations - digest VideoDigest @relation(fields: [digestId], references: [id]) - video Video @relation(fields: [videoId], references: [id]) - folder DigestOutputFolder? @relation(fields: [folderId], references: [id]) + digest VideoDigest @relation(fields: [digestId], references: [id]) + video Video @relation(fields: [videoId], references: [id]) + folder DigestOutputFolder? @relation(fields: [folderId], references: [id]) @@index([digestId], map: "idx_digest_compilations_digest") @@index([videoId], map: "idx_digest_compilations_video") @@ -2973,23 +2988,23 @@ model DigestCompilation { } model DigestOutputFolder { - id Int @id @default(autoincrement()) - digestId Int @map("digest_id") - videoId Int @map("video_id") - folderPath String @map("folder_path") - folderName String @map("folder_name") - folderType String @default("clips") @map("folder_type") - clipCount Int? @default(0) @map("clip_count") - compilationCount Int? @default(0) @map("compilation_count") - totalSize Int? @map("total_size") - createdAt DateTime @default(now()) @map("created_at") + id Int @id @default(autoincrement()) + digestId Int @map("digest_id") + videoId Int @map("video_id") + folderPath String @map("folder_path") + folderName String @map("folder_name") + folderType String @default("clips") @map("folder_type") + clipCount Int? @default(0) @map("clip_count") + compilationCount Int? @default(0) @map("compilation_count") + totalSize Int? @map("total_size") + createdAt DateTime @default(now()) @map("created_at") // Relations - digest VideoDigest @relation(fields: [digestId], references: [id]) - video Video @relation(fields: [videoId], references: [id]) - generatedClips DigestGeneratedClip[] - generatedScenes DigestGeneratedScene[] - compilations DigestCompilation[] + digest VideoDigest @relation(fields: [digestId], references: [id]) + video Video @relation(fields: [videoId], references: [id]) + generatedClips DigestGeneratedClip[] + generatedScenes DigestGeneratedScene[] + compilations DigestCompilation[] @@index([digestId], map: "idx_digest_output_folders_digest") @@index([videoId], map: "idx_digest_output_folders_video") @@ -3014,9 +3029,9 @@ model DigestGeneratedScene { publishedAt DateTime? @map("published_at") // Relations - digest VideoDigest @relation(fields: [digestId], references: [id]) - video Video @relation(fields: [videoId], references: [id]) - folder DigestOutputFolder? @relation(fields: [folderId], references: [id]) + digest VideoDigest @relation(fields: [digestId], references: [id]) + video Video @relation(fields: [videoId], references: [id]) + folder DigestOutputFolder? @relation(fields: [folderId], references: [id]) @@index([digestId], map: "idx_digest_generated_scenes_digest") @@index([videoId], map: "idx_digest_generated_scenes_video") @@ -3026,22 +3041,22 @@ model DigestGeneratedScene { } model VideoSceneCut { - id Int @id @default(autoincrement()) - videoId Int @map("video_id") - cuts Json - sceneCount Int @map("scene_count") - duration Float @db.Real - detector String @default("content") - threshold Float @default(27.0) @db.Real - transnetCuts Json? @map("transnet_cuts") - pyscenedetectCuts Json? @map("pyscenedetect_cuts") - clipCuts Json? @map("clip_cuts") - mergedCuts Json? @map("merged_cuts") - analysisMetadata Json? @map("analysis_metadata") - createdAt DateTime @default(now()) @map("created_at") + id Int @id @default(autoincrement()) + videoId Int @map("video_id") + cuts Json + sceneCount Int @map("scene_count") + duration Float @db.Real + detector String @default("content") + threshold Float @default(27.0) @db.Real + transnetCuts Json? @map("transnet_cuts") + pyscenedetectCuts Json? @map("pyscenedetect_cuts") + clipCuts Json? @map("clip_cuts") + mergedCuts Json? @map("merged_cuts") + analysisMetadata Json? @map("analysis_metadata") + createdAt DateTime @default(now()) @map("created_at") // Relations - video Video @relation(fields: [videoId], references: [id]) + video Video @relation(fields: [videoId], references: [id]) @@index([videoId], map: "idx_video_scene_cuts_video") @@index([detector], map: "idx_video_scene_cuts_detector") @@ -3060,7 +3075,7 @@ model VideoTagTimeline { createdAt DateTime @default(now()) @map("created_at") // Relations - video Video @relation(fields: [videoId], references: [id]) + video Video @relation(fields: [videoId], references: [id]) @@index([videoId], map: "idx_video_tag_timeline_video") @@index([category], map: "idx_video_tag_timeline_category") @@ -3071,21 +3086,21 @@ model VideoTagTimeline { } model VideoSegment { - id Int @id @default(autoincrement()) - videoId Int @map("video_id") - segmentType SegmentType @map("segment_type") - startTime Float @map("start_time") @db.Real - endTime Float @map("end_time") @db.Real - duration Float @db.Real - tags Json? - vocalCategory VocalCategory? @map("vocal_category") - transcript String? - dominantPosition String? @map("dominant_position") - interestScore Int? @map("interest_score") - createdAt DateTime @default(now()) @map("created_at") + id Int @id @default(autoincrement()) + videoId Int @map("video_id") + segmentType SegmentType @map("segment_type") + startTime Float @map("start_time") @db.Real + endTime Float @map("end_time") @db.Real + duration Float @db.Real + tags Json? + vocalCategory VocalCategory? @map("vocal_category") + transcript String? + dominantPosition String? @map("dominant_position") + interestScore Int? @map("interest_score") + createdAt DateTime @default(now()) @map("created_at") // Relations - video Video @relation(fields: [videoId], references: [id]) + video Video @relation(fields: [videoId], references: [id]) @@index([videoId], map: "idx_video_segments_video") @@index([segmentType], map: "idx_video_segments_type") @@ -3105,7 +3120,7 @@ model VideoTag { createdAt DateTime @default(now()) @map("created_at") // Relations - video Video @relation(fields: [videoId], references: [id]) + video Video @relation(fields: [videoId], references: [id]) @@index([videoId], map: "idx_video_tags_video") @@index([category], map: "idx_video_tags_category") @@ -3115,19 +3130,19 @@ model VideoTag { } model DigestSuggestedTag { - id Int @id @default(autoincrement()) - digestId Int @map("digest_id") - category String - value String - confidence Int? - evidence Json? - status SuggestedTagStatus @default(pending) - mappedTagId Int? @map("mapped_tag_id") - createdAt DateTime @default(now()) @map("created_at") - reviewedAt DateTime? @map("reviewed_at") + id Int @id @default(autoincrement()) + digestId Int @map("digest_id") + category String + value String + confidence Int? + evidence Json? + status SuggestedTagStatus @default(pending) + mappedTagId Int? @map("mapped_tag_id") + createdAt DateTime @default(now()) @map("created_at") + reviewedAt DateTime? @map("reviewed_at") // Relations - digest VideoDigest @relation(fields: [digestId], references: [id]) + digest VideoDigest @relation(fields: [digestId], references: [id]) @@index([digestId], map: "idx_digest_suggested_tags_digest") @@index([status], map: "idx_digest_suggested_tags_status") @@ -3149,7 +3164,7 @@ model DigestClipTag { createdAt DateTime @default(now()) @map("created_at") // Relations - video Video @relation(fields: [videoId], references: [id]) + video Video @relation(fields: [videoId], references: [id]) @@index([digestId], map: "idx_digest_clip_tags_digest") @@index([clipId], map: "idx_digest_clip_tags_clip") @@ -3164,22 +3179,22 @@ model DigestClipTag { // ============================================================================ model WatchPartySession { - id Int @id @default(autoincrement()) - hostUserId String @map("host_user_id") - mediaId Int @map("media_id") - status WatchPartyStatus @default(active) - inviteCode String @unique @map("invite_code") - currentTime Int @default(0) @map("current_time") - isPlaying Boolean @default(false) @map("is_playing") - createdAt DateTime @default(now()) @map("created_at") - endedAt DateTime? @map("ended_at") + id Int @id @default(autoincrement()) + hostUserId String @map("host_user_id") + mediaId Int @map("media_id") + status WatchPartyStatus @default(active) + inviteCode String @unique @map("invite_code") + currentTime Int @default(0) @map("current_time") + isPlaying Boolean @default(false) @map("is_playing") + createdAt DateTime @default(now()) @map("created_at") + endedAt DateTime? @map("ended_at") // Relations - host User @relation("WatchPartyHost", fields: [hostUserId], references: [id]) + host User @relation("WatchPartyHost", fields: [hostUserId], references: [id]) participants WatchPartyParticipant[] - messages WatchPartyChatMessage[] - reactions WatchPartyReaction[] - invites WatchPartyInvite[] + messages WatchPartyChatMessage[] + reactions WatchPartyReaction[] + invites WatchPartyInvite[] @@index([hostUserId], map: "idx_watch_party_sessions_host") @@index([mediaId], map: "idx_watch_party_sessions_media") @@ -3189,15 +3204,15 @@ model WatchPartySession { } model WatchPartyParticipant { - id Int @id @default(autoincrement()) - sessionId Int @map("session_id") - userId String @map("user_id") - joinedAt DateTime @default(now()) @map("joined_at") + id Int @id @default(autoincrement()) + sessionId Int @map("session_id") + userId String @map("user_id") + joinedAt DateTime @default(now()) @map("joined_at") leftAt DateTime? @map("left_at") // Relations - session WatchPartySession @relation(fields: [sessionId], references: [id]) - user User @relation("WatchPartyParticipant", fields: [userId], references: [id]) + session WatchPartySession @relation(fields: [sessionId], references: [id]) + user User @relation("WatchPartyParticipant", fields: [userId], references: [id]) @@index([sessionId], map: "idx_watch_party_participants_session") @@index([userId], map: "idx_watch_party_participants_user") @@ -3212,9 +3227,9 @@ model WatchPartyChatMessage { createdAt DateTime @default(now()) @map("created_at") // Relations - session WatchPartySession @relation(fields: [sessionId], references: [id]) - user User @relation("WatchPartyChatUser", fields: [userId], references: [id]) - comment Comment @relation(fields: [commentId], references: [id]) + session WatchPartySession @relation(fields: [sessionId], references: [id]) + user User @relation("WatchPartyChatUser", fields: [userId], references: [id]) + comment Comment @relation(fields: [commentId], references: [id]) @@index([sessionId], map: "idx_watch_party_chat_session") @@index([userId], map: "idx_watch_party_chat_user") @@ -3222,16 +3237,16 @@ model WatchPartyChatMessage { } model WatchPartyReaction { - id Int @id @default(autoincrement()) - sessionId Int @map("session_id") - userId String @map("user_id") + id Int @id @default(autoincrement()) + sessionId Int @map("session_id") + userId String @map("user_id") reactionType ReactionType @map("reaction_type") - videoTimestamp Int @map("video_timestamp") - createdAt DateTime @default(now()) @map("created_at") + videoTimestamp Int @map("video_timestamp") + createdAt DateTime @default(now()) @map("created_at") // Relations - session WatchPartySession @relation(fields: [sessionId], references: [id]) - user User @relation("WatchPartyReactionUser", fields: [userId], references: [id]) + session WatchPartySession @relation(fields: [sessionId], references: [id]) + user User @relation("WatchPartyReactionUser", fields: [userId], references: [id]) @@index([sessionId], map: "idx_watch_party_reactions_session") @@index([userId], map: "idx_watch_party_reactions_user") @@ -3239,18 +3254,18 @@ model WatchPartyReaction { } model WatchPartyInvite { - id Int @id @default(autoincrement()) - sessionId Int @map("session_id") - userId String @map("user_id") - invitedBy String @map("invited_by") - status String @default("pending") - createdAt DateTime @default(now()) @map("created_at") + id Int @id @default(autoincrement()) + sessionId Int @map("session_id") + userId String @map("user_id") + invitedBy String @map("invited_by") + status String @default("pending") + createdAt DateTime @default(now()) @map("created_at") acceptedAt DateTime? @map("accepted_at") // Relations - session WatchPartySession @relation(fields: [sessionId], references: [id]) - user User @relation("WatchPartyInvitee", fields: [userId], references: [id]) - inviter User @relation("WatchPartyInviter", fields: [invitedBy], references: [id]) + session WatchPartySession @relation(fields: [sessionId], references: [id]) + user User @relation("WatchPartyInvitee", fields: [userId], references: [id]) + inviter User @relation("WatchPartyInviter", fields: [invitedBy], references: [id]) @@index([sessionId], map: "idx_watch_party_invites_session") @@index([userId], map: "idx_watch_party_invites_user") @@ -3262,21 +3277,21 @@ model WatchPartyInvite { // ============================================================================ model TagGenerationJob { - id Int @id @default(autoincrement()) - videoId Int @map("video_id") - jobId Int? @map("job_id") - status String @default("pending") - progress Int? @default(0) - generatedTags Json? @map("generated_tags") - rawResponse String? @map("raw_response") - error String? - createdAt DateTime @default(now()) @map("created_at") - startedAt DateTime? @map("started_at") - completedAt DateTime? @map("completed_at") + id Int @id @default(autoincrement()) + videoId Int @map("video_id") + jobId Int? @map("job_id") + status String @default("pending") + progress Int? @default(0) + generatedTags Json? @map("generated_tags") + rawResponse String? @map("raw_response") + error String? + createdAt DateTime @default(now()) @map("created_at") + startedAt DateTime? @map("started_at") + completedAt DateTime? @map("completed_at") // Relations - video Video @relation(fields: [videoId], references: [id]) - job Job? @relation(fields: [jobId], references: [id]) + video Video @relation(fields: [videoId], references: [id]) + job Job? @relation(fields: [jobId], references: [id]) @@index([videoId], map: "idx_tag_generation_jobs_video") @@index([status], map: "idx_tag_generation_jobs_status") @@ -3288,16 +3303,16 @@ model TagGenerationJob { // ============================================================================ model Creator { - id Int @id @default(autoincrement()) - name String @unique - stage_name String? @map("stage_name") - performerGender String? @map("performer_gender") - faceEmbedding Json? @map("face_embedding") - referenceImagePath String? @map("reference_image_path") - performerCategory String? @map("performer_category") - status String @default("active") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime? @map("updated_at") + id Int @id @default(autoincrement()) + name String @unique + stage_name String? @map("stage_name") + performerGender String? @map("performer_gender") + faceEmbedding Json? @map("face_embedding") + referenceImagePath String? @map("reference_image_path") + performerCategory String? @map("performer_category") + status String @default("active") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime? @map("updated_at") // Relations publicMediaPerformers PublicMediaPerformer[] @@ -3310,22 +3325,22 @@ model Creator { } model PerformerFace { - id Int @id @default(autoincrement()) - performerId Int @map("performer_id") - sourcePath String @map("source_path") - frameTimestamp Float? @map("frame_timestamp") @db.Real - bbox Json - embedding Json - gender Int - age Int - detScore Float @map("det_score") @db.Real - verifiedByUser Boolean @default(false) @map("verified_by_user") - confidence Float? @db.Real - isReferenceImage Boolean @default(false) @map("is_reference_image") - createdAt DateTime @default(now()) @map("created_at") + id Int @id @default(autoincrement()) + performerId Int @map("performer_id") + sourcePath String @map("source_path") + frameTimestamp Float? @map("frame_timestamp") @db.Real + bbox Json + embedding Json + gender Int + age Int + detScore Float @map("det_score") @db.Real + verifiedByUser Boolean @default(false) @map("verified_by_user") + confidence Float? @db.Real + isReferenceImage Boolean @default(false) @map("is_reference_image") + createdAt DateTime @default(now()) @map("created_at") // Relations - performer Creator @relation(fields: [performerId], references: [id]) + performer Creator @relation(fields: [performerId], references: [id]) @@index([performerId], map: "idx_performer_faces_performer") @@index([sourcePath], map: "idx_performer_faces_source") @@ -3334,23 +3349,23 @@ model PerformerFace { } model PerformerDiscrepancy { - id Int @id @default(autoincrement()) - videoPath String @map("video_path") - frameTimestamp Float @map("frame_timestamp") @db.Real - assignedPerformerId Int @map("assigned_performer_id") - detectedPerformerId Int? @map("detected_performer_id") - similarity Float @map("similarity") @db.Real - faceData Json @map("face_data") - resolutionStatus String @default("pending") @map("resolution_status") - resolutionAction String? @map("resolution_action") - resolutionNotes String? @map("resolution_notes") - resolvedBy String? @map("resolved_by") - resolvedAt DateTime? @map("resolved_at") - createdAt DateTime @default(now()) @map("created_at") + id Int @id @default(autoincrement()) + videoPath String @map("video_path") + frameTimestamp Float @map("frame_timestamp") @db.Real + assignedPerformerId Int @map("assigned_performer_id") + detectedPerformerId Int? @map("detected_performer_id") + similarity Float @map("similarity") @db.Real + faceData Json @map("face_data") + resolutionStatus String @default("pending") @map("resolution_status") + resolutionAction String? @map("resolution_action") + resolutionNotes String? @map("resolution_notes") + resolvedBy String? @map("resolved_by") + resolvedAt DateTime? @map("resolved_at") + createdAt DateTime @default(now()) @map("created_at") // Relations - assignedPerformer Creator @relation(fields: [assignedPerformerId], references: [id]) - resolver User? @relation("PerformerDiscrepancyResolver", fields: [resolvedBy], references: [id]) + assignedPerformer Creator @relation(fields: [assignedPerformerId], references: [id]) + resolver User? @relation("PerformerDiscrepancyResolver", fields: [resolvedBy], references: [id]) @@index([assignedPerformerId], map: "idx_performer_discrepancies_assigned") @@index([detectedPerformerId], map: "idx_performer_discrepancies_detected") @@ -3363,21 +3378,21 @@ model PerformerDiscrepancy { // ============================================================================ model Pipeline { - id Int @id @default(autoincrement()) - name String - description String? - status PipelineStatus @default(draft) - config Json? - templateId Int? @map("template_id") - createdAt DateTime @default(now()) @map("created_at") - startedAt DateTime? @map("started_at") - completedAt DateTime? @map("completed_at") - error String? + id Int @id @default(autoincrement()) + name String + description String? + status PipelineStatus @default(draft) + config Json? + templateId Int? @map("template_id") + createdAt DateTime @default(now()) @map("created_at") + startedAt DateTime? @map("started_at") + completedAt DateTime? @map("completed_at") + error String? // Relations - template PipelineTemplate? @relation(fields: [templateId], references: [id]) - steps PipelineStep[] - jobs Job[] + template PipelineTemplate? @relation(fields: [templateId], references: [id]) + steps PipelineStep[] + jobs Job[] @@index([status], map: "idx_pipelines_status") @@index([templateId], map: "idx_pipelines_template") @@ -3385,25 +3400,25 @@ model Pipeline { } model PipelineStep { - id Int @id @default(autoincrement()) - pipelineId Int @map("pipeline_id") - stepName String @map("step_name") - stepType String @map("step_type") - sequenceOrder Int @map("sequence_order") - status PipelineStepStatus @default(pending) - config Json? - inputs Json? - outputs Json? - dependsOn Json? @map("depends_on") - error String? - createdAt DateTime @default(now()) @map("created_at") - startedAt DateTime? @map("started_at") - completedAt DateTime? @map("completed_at") + id Int @id @default(autoincrement()) + pipelineId Int @map("pipeline_id") + stepName String @map("step_name") + stepType String @map("step_type") + sequenceOrder Int @map("sequence_order") + status PipelineStepStatus @default(pending) + config Json? + inputs Json? + outputs Json? + dependsOn Json? @map("depends_on") + error String? + createdAt DateTime @default(now()) @map("created_at") + startedAt DateTime? @map("started_at") + completedAt DateTime? @map("completed_at") // Relations - pipeline Pipeline @relation(fields: [pipelineId], references: [id]) - jobs Job[] - events PipelineStepEvent[] + pipeline Pipeline @relation(fields: [pipelineId], references: [id]) + jobs Job[] + events PipelineStepEvent[] @@index([pipelineId], map: "idx_pipeline_steps_pipeline") @@index([status], map: "idx_pipeline_steps_status") @@ -3412,15 +3427,15 @@ model PipelineStep { } model PipelineStepEvent { - id Int @id @default(autoincrement()) - stepId Int @map("step_id") - eventType String @map("event_type") - message String? - data Json? - createdAt DateTime @default(now()) @map("created_at") + id Int @id @default(autoincrement()) + stepId Int @map("step_id") + eventType String @map("event_type") + message String? + data Json? + createdAt DateTime @default(now()) @map("created_at") // Relations - step PipelineStep @relation(fields: [stepId], references: [id]) + step PipelineStep @relation(fields: [stepId], references: [id]) @@index([stepId], map: "idx_pipeline_step_events_step") @@index([eventType], map: "idx_pipeline_step_events_type") @@ -3444,18 +3459,18 @@ model ResourceSnapshot { } model PipelineTemplate { - id Int @id @default(autoincrement()) - name String - description String? - category String? - defaultConfig Json? @map("default_config") - stepDefinitions Json @map("step_definitions") - isPublic Boolean @default(false) @map("is_public") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime? @map("updated_at") + id Int @id @default(autoincrement()) + name String + description String? + category String? + defaultConfig Json? @map("default_config") + stepDefinitions Json @map("step_definitions") + isPublic Boolean @default(false) @map("is_public") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime? @map("updated_at") // Relations - pipelines Pipeline[] + pipelines Pipeline[] @@index([category], map: "idx_pipeline_templates_category") @@index([isPublic], map: "idx_pipeline_templates_public") @@ -3467,31 +3482,31 @@ model PipelineTemplate { // ============================================================================ model SubscriptionPlan { - id Int @id @default(autoincrement()) - name String - priceCAD Int @map("price_cad") - durationDays Int @map("duration_days") - features Json? - isActive Boolean @default(true) @map("is_active") - createdAt DateTime @default(now()) @map("created_at") + id Int @id @default(autoincrement()) + name String + priceCAD Int @map("price_cad") + durationDays Int @map("duration_days") + features Json? + isActive Boolean @default(true) @map("is_active") + createdAt DateTime @default(now()) @map("created_at") // Stripe integration - stripeProductId String? @map("stripe_product_id") - stripePriceId String? @map("stripe_price_id") - stripeYearlyPriceId String? @map("stripe_yearly_price_id") - yearlyPriceCAD Int? @map("yearly_price_cad") - description String? @db.Text - tier Int @default(0) - displayOrder Int @default(0) @map("display_order") + stripeProductId String? @map("stripe_product_id") + stripePriceId String? @map("stripe_price_id") + stripeYearlyPriceId String? @map("stripe_yearly_price_id") + yearlyPriceCAD Int? @map("yearly_price_cad") + description String? @db.Text + tier Int @default(0) + displayOrder Int @default(0) @map("display_order") // Page content fields - slug String? @unique - coverPhoto String? @map("cover_photo") - coverVideoId Int? @map("cover_video_id") - richDescription String? @db.Text @map("rich_description") - ctaText String? @map("cta_text") - ctaSubtext String? @map("cta_subtext") - highlightPlan Boolean @default(false) @map("highlight_plan") + slug String? @unique + coverPhoto String? @map("cover_photo") + coverVideoId Int? @map("cover_video_id") + richDescription String? @map("rich_description") @db.Text + ctaText String? @map("cta_text") + ctaSubtext String? @map("cta_subtext") + highlightPlan Boolean @default(false) @map("highlight_plan") // Relations subscriptions UserSubscription[] @@ -3500,24 +3515,24 @@ model SubscriptionPlan { } model UserSubscription { - id Int @id @default(autoincrement()) - userId String @map("user_id") - planId Int @map("plan_id") - status SubscriptionStatus @default(active) - startDate DateTime @map("start_date") - endDate DateTime @map("end_date") - cancelledAt DateTime? @map("cancelled_at") - createdAt DateTime @default(now()) @map("created_at") + id Int @id @default(autoincrement()) + userId String @map("user_id") + planId Int @map("plan_id") + status SubscriptionStatus @default(active) + startDate DateTime @map("start_date") + endDate DateTime @map("end_date") + cancelledAt DateTime? @map("cancelled_at") + createdAt DateTime @default(now()) @map("created_at") // Stripe integration - stripeSubscriptionId String? @unique @map("stripe_subscription_id") - stripeCustomerId String? @map("stripe_customer_id") - currentPeriodEnd DateTime? @map("current_period_end") - cancelAtPeriodEnd Boolean @default(false) @map("cancel_at_period_end") + stripeSubscriptionId String? @unique @map("stripe_subscription_id") + stripeCustomerId String? @map("stripe_customer_id") + currentPeriodEnd DateTime? @map("current_period_end") + cancelAtPeriodEnd Boolean @default(false) @map("cancel_at_period_end") // Relations - user User @relation("UserSubscriptions", fields: [userId], references: [id]) - plan SubscriptionPlan @relation(fields: [planId], references: [id]) + user User @relation("UserSubscriptions", fields: [userId], references: [id]) + plan SubscriptionPlan @relation(fields: [planId], references: [id]) @@index([userId], map: "idx_user_subscriptions_user") @@index([planId], map: "idx_user_subscriptions_plan") @@ -3526,24 +3541,24 @@ model UserSubscription { } model Invoice { - id Int @id @default(autoincrement()) - userId String @map("user_id") - subscriptionId Int? @map("subscription_id") - amountCAD Int @map("amount_cad") - status InvoiceStatus @default(pending) - issuedAt DateTime @default(now()) @map("issued_at") - paidAt DateTime? @map("paid_at") - dueDate DateTime @map("due_date") - description String? - metadata Json? + id Int @id @default(autoincrement()) + userId String @map("user_id") + subscriptionId Int? @map("subscription_id") + amountCAD Int @map("amount_cad") + status InvoiceStatus @default(pending) + issuedAt DateTime @default(now()) @map("issued_at") + paidAt DateTime? @map("paid_at") + dueDate DateTime @map("due_date") + description String? + metadata Json? // Stripe integration - stripeInvoiceId String? @unique @map("stripe_invoice_id") - type String @default("subscription") @map("invoice_type") // subscription|product|donation + stripeInvoiceId String? @unique @map("stripe_invoice_id") + type String @default("subscription") @map("invoice_type") // subscription|product|donation // Relations - user User @relation("UserInvoices", fields: [userId], references: [id]) - payments Payment[] + user User @relation("UserInvoices", fields: [userId], references: [id]) + payments Payment[] @@index([userId], map: "idx_invoices_user") @@index([status], map: "idx_invoices_status") @@ -3552,25 +3567,25 @@ model Invoice { } model Payment { - id Int @id @default(autoincrement()) - invoiceId Int @map("invoice_id") - userId String @map("user_id") - amountCAD Int @map("amount_cad") - method PaymentMethod - status PaymentStatus @default(pending) - externalId String? @map("external_id") - metadata Json? - processedAt DateTime? @map("processed_at") - createdAt DateTime @default(now()) @map("created_at") + id Int @id @default(autoincrement()) + invoiceId Int @map("invoice_id") + userId String @map("user_id") + amountCAD Int @map("amount_cad") + method PaymentMethod + status PaymentStatus @default(pending) + externalId String? @map("external_id") + metadata Json? + processedAt DateTime? @map("processed_at") + createdAt DateTime @default(now()) @map("created_at") // Stripe integration - stripePaymentIntentId String? @unique @map("stripe_payment_intent_id") - stripeCheckoutSessionId String? @map("stripe_checkout_session_id") + stripePaymentIntentId String? @unique @map("stripe_payment_intent_id") + stripeCheckoutSessionId String? @map("stripe_checkout_session_id") // Relations - invoice Invoice @relation(fields: [invoiceId], references: [id]) - user User @relation("UserPayments", fields: [userId], references: [id]) - auditLogs PaymentAuditLog[] + invoice Invoice @relation(fields: [invoiceId], references: [id]) + user User @relation("UserPayments", fields: [userId], references: [id]) + auditLogs PaymentAuditLog[] @@index([invoiceId], map: "idx_payments_invoice") @@index([userId], map: "idx_payments_user") @@ -3579,20 +3594,20 @@ model Payment { } model PaymentAuditLog { - id Int @id @default(autoincrement()) - paymentId Int? @map("payment_id") - orderId String? @map("order_id") - action String - oldStatus String? @map("old_status") - newStatus String? @map("new_status") - userId String? @map("user_id") - metadata Json? - createdAt DateTime @default(now()) @map("created_at") + id Int @id @default(autoincrement()) + paymentId Int? @map("payment_id") + orderId String? @map("order_id") + action String + oldStatus String? @map("old_status") + newStatus String? @map("new_status") + userId String? @map("user_id") + metadata Json? + createdAt DateTime @default(now()) @map("created_at") // Relations - payment Payment? @relation(fields: [paymentId], references: [id]) - order Order? @relation(fields: [orderId], references: [id]) - user User? @relation("PaymentAuditUser", fields: [userId], references: [id]) + payment Payment? @relation(fields: [paymentId], references: [id]) + order Order? @relation(fields: [orderId], references: [id]) + user User? @relation("PaymentAuditUser", fields: [userId], references: [id]) @@index([paymentId], map: "idx_payment_audit_log_payment") @@index([orderId], map: "idx_payment_audit_log_order") @@ -3602,29 +3617,29 @@ model PaymentAuditLog { } model Product { - id String @id @default(cuid()) - slug String @unique - title String - description String? @db.Text - priceCAD Int @map("price_cad") - type ProductType - stripeProductId String? @map("stripe_product_id") - stripePriceId String? @map("stripe_price_id") - isActive Boolean @default(true) @map("is_active") - imageUrl String? @map("image_url") - photoId Int? @map("photo_id") - videoId Int? @map("video_id") - galleryPhotoIds Json? @map("gallery_photo_ids") - downloadUrl String? @map("download_url") - metadata Json? - maxPurchases Int? @map("max_purchases") - purchaseCount Int @default(0) @map("purchase_count") - createdByUserId String? @map("created_by_user_id") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") + id String @id @default(cuid()) + slug String @unique + title String + description String? @db.Text + priceCAD Int @map("price_cad") + type ProductType + stripeProductId String? @map("stripe_product_id") + stripePriceId String? @map("stripe_price_id") + isActive Boolean @default(true) @map("is_active") + imageUrl String? @map("image_url") + photoId Int? @map("photo_id") + videoId Int? @map("video_id") + galleryPhotoIds Json? @map("gallery_photo_ids") + downloadUrl String? @map("download_url") + metadata Json? + maxPurchases Int? @map("max_purchases") + purchaseCount Int @default(0) @map("purchase_count") + createdByUserId String? @map("created_by_user_id") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") - orders Order[] - galleryAd Ad? + orders Order[] + galleryAd Ad? @@index([type], map: "idx_products_type") @@index([isActive], map: "idx_products_active") @@ -3632,36 +3647,36 @@ model Product { } model Order { - id String @id @default(cuid()) - userId String? @map("user_id") - productId String? @map("product_id") - amountCAD Int @map("amount_cad") - status OrderStatus @default(PENDING) - stripeCheckoutSessionId String? @unique @map("stripe_checkout_session_id") - stripePaymentIntentId String? @map("stripe_payment_intent_id") - type String @default("product") @map("order_type") // product|donation + id String @id @default(cuid()) + userId String? @map("user_id") + productId String? @map("product_id") + amountCAD Int @map("amount_cad") + status OrderStatus @default(PENDING) + stripeCheckoutSessionId String? @unique @map("stripe_checkout_session_id") + stripePaymentIntentId String? @map("stripe_payment_intent_id") + type String @default("product") @map("order_type") // product|donation // Buyer info (for guests) - buyerEmail String @map("buyer_email") - buyerName String? @map("buyer_name") + buyerEmail String @map("buyer_email") + buyerName String? @map("buyer_name") // Donation-specific - donorMessage String? @db.Text @map("donor_message") - isAnonymous Boolean @default(false) @map("is_anonymous") + donorMessage String? @map("donor_message") @db.Text + isAnonymous Boolean @default(false) @map("is_anonymous") - completedAt DateTime? @map("completed_at") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") + completedAt DateTime? @map("completed_at") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") // Relations - user User? @relation("UserOrders", fields: [userId], references: [id]) - product Product? @relation(fields: [productId], references: [id]) - donationPageId String? @map("donation_page_id") - donationPage DonationPage? @relation("DonationPageOrders", fields: [donationPageId], references: [id], onDelete: SetNull) - influenceCampaignId String? @map("influence_campaign_id") - influenceCampaign Campaign? @relation("CampaignDonations", fields: [influenceCampaignId], references: [id], onDelete: SetNull) - tickets Ticket[] @relation("TicketOrder") - auditLogs PaymentAuditLog[] + user User? @relation("UserOrders", fields: [userId], references: [id]) + product Product? @relation(fields: [productId], references: [id]) + donationPageId String? @map("donation_page_id") + donationPage DonationPage? @relation("DonationPageOrders", fields: [donationPageId], references: [id], onDelete: SetNull) + influenceCampaignId String? @map("influence_campaign_id") + influenceCampaign Campaign? @relation("CampaignDonations", fields: [influenceCampaignId], references: [id], onDelete: SetNull) + tickets Ticket[] @relation("TicketOrder") + auditLogs PaymentAuditLog[] @@index([userId], map: "idx_orders_user") @@index([productId], map: "idx_orders_product") @@ -3680,57 +3695,57 @@ enum DonationPageStatus { } model DonationPage { - id String @id @default(cuid()) - slug String @unique - title String - description String? @db.Text - status DonationPageStatus @default(DRAFT) + id String @id @default(cuid()) + slug String @unique + title String + description String? @db.Text + status DonationPageStatus @default(DRAFT) // Donation config (per-page, mirrors PaymentSettings fields) - suggestedAmounts Json @default("[1000, 2500, 5000, 10000]") - minimumAmount Int @default(500) @map("minimum_amount") - thankYouMessage String @default("Thank you for your support!") @db.Text @map("thank_you_message") + suggestedAmounts Json @default("[1000, 2500, 5000, 10000]") + minimumAmount Int @default(500) @map("minimum_amount") + thankYouMessage String @default("Thank you for your support!") @map("thank_you_message") @db.Text // Media - coverPhoto String? @map("cover_photo") - coverVideoId Int? @map("cover_video_id") + coverPhoto String? @map("cover_photo") + coverVideoId Int? @map("cover_video_id") // Display options - highlightPage Boolean @default(false) @map("highlight_page") - showDonorCount Boolean @default(true) @map("show_donor_count") - showTotalRaised Boolean @default(false) @map("show_total_raised") - goalAmount Int? @map("goal_amount") + highlightPage Boolean @default(false) @map("highlight_page") + showDonorCount Boolean @default(true) @map("show_donor_count") + showTotalRaised Boolean @default(false) @map("show_total_raised") + goalAmount Int? @map("goal_amount") // Creator tracking - createdByUserId String? @map("created_by_user_id") - createdByUser User? @relation("DonationPageCreator", fields: [createdByUserId], references: [id], onDelete: SetNull) + createdByUserId String? @map("created_by_user_id") + createdByUser User? @relation("DonationPageCreator", fields: [createdByUserId], references: [id], onDelete: SetNull) - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") - orders Order[] @relation("DonationPageOrders") + orders Order[] @relation("DonationPageOrders") @@index([status]) @@map("donation_pages") } model PaymentSettings { - id String @id @default(cuid()) - stripeSecretKey String @default("") @map("stripe_secret_key") - stripePublishableKey String @default("") @map("stripe_publishable_key") - stripeWebhookSecret String @default("") @map("stripe_webhook_secret") - defaultCurrency String @default("cad") @map("default_currency") + id String @id @default(cuid()) + stripeSecretKey String @default("") @map("stripe_secret_key") + stripePublishableKey String @default("") @map("stripe_publishable_key") + stripeWebhookSecret String @default("") @map("stripe_webhook_secret") + defaultCurrency String @default("cad") @map("default_currency") // Donation settings - enableDonations Boolean @default(true) @map("enable_donations") - donationSuggestedAmounts Json @default("[1000, 2500, 5000, 10000]") @map("donation_suggested_amounts") - donationMinimum Int @default(500) @map("donation_minimum") - donationPageTitle String @default("Support Our Work") @map("donation_page_title") - donationPageDescription String? @db.Text @map("donation_page_description") - thankYouMessage String @default("Thank you for your support!") @db.Text @map("thank_you_message") + enableDonations Boolean @default(true) @map("enable_donations") + donationSuggestedAmounts Json @default("[1000, 2500, 5000, 10000]") @map("donation_suggested_amounts") + donationMinimum Int @default(500) @map("donation_minimum") + donationPageTitle String @default("Support Our Work") @map("donation_page_title") + donationPageDescription String? @map("donation_page_description") @db.Text + thankYouMessage String @default("Thank you for your support!") @map("thank_you_message") @db.Text - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") @@map("payment_settings") } @@ -3740,18 +3755,18 @@ model PaymentSettings { // ============================================================================ model Notification { - id Int @id @default(autoincrement()) - userId String @map("user_id") - type NotificationType - title String - message String - metadata Json? - isRead Boolean @default(false) @map("is_read") - createdAt DateTime @default(now()) @map("created_at") - readAt DateTime? @map("read_at") + id Int @id @default(autoincrement()) + userId String @map("user_id") + type NotificationType + title String + message String + metadata Json? + isRead Boolean @default(false) @map("is_read") + createdAt DateTime @default(now()) @map("created_at") + readAt DateTime? @map("read_at") // Relations - user User @relation("UserNotifications", fields: [userId], references: [id]) + user User @relation("UserNotifications", fields: [userId], references: [id]) @@index([userId], map: "idx_notifications_user") @@index([isRead], map: "idx_notifications_read") @@ -3761,21 +3776,21 @@ model Notification { } model NotificationPreferences { - id Int @id @default(autoincrement()) - userId String @unique @map("user_id") - enableFriendRequests Boolean @default(true) @map("enable_friend_requests") - enableComments Boolean @default(true) @map("enable_comments") - enableUploadApprovals Boolean @default(true) @map("enable_upload_approvals") - enableAchievements Boolean @default(true) @map("enable_achievements") - enableSystemUpdates Boolean @default(true) @map("enable_system_updates") - emailNotifications Boolean @default(false) @map("email_notifications") - digestFrequency String @default("none") @map("digest_frequency") // "none" | "daily" | "weekly" - lastDigestSentAt DateTime? @map("last_digest_sent_at") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime? @map("updated_at") + id Int @id @default(autoincrement()) + userId String @unique @map("user_id") + enableFriendRequests Boolean @default(true) @map("enable_friend_requests") + enableComments Boolean @default(true) @map("enable_comments") + enableUploadApprovals Boolean @default(true) @map("enable_upload_approvals") + enableAchievements Boolean @default(true) @map("enable_achievements") + enableSystemUpdates Boolean @default(true) @map("enable_system_updates") + emailNotifications Boolean @default(false) @map("email_notifications") + digestFrequency String @default("none") @map("digest_frequency") // "none" | "daily" | "weekly" + lastDigestSentAt DateTime? @map("last_digest_sent_at") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime? @map("updated_at") // Relations - user User @relation("NotificationPreferences", fields: [userId], references: [id]) + user User @relation("NotificationPreferences", fields: [userId], references: [id]) @@index([userId], map: "idx_notification_preferences_user") @@map("notification_preferences") @@ -3786,13 +3801,13 @@ model NotificationPreferences { // ============================================================================ model GeoBlockingRule { - id Int @id @default(autoincrement()) - countryCode String @map("country_code") - action String // 'block' | 'allow' - reason String? - isActive Boolean @default(true) @map("is_active") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime? @map("updated_at") + id Int @id @default(autoincrement()) + countryCode String @map("country_code") + action String // 'block' | 'allow' + reason String? + isActive Boolean @default(true) @map("is_active") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime? @map("updated_at") @@index([countryCode], map: "idx_geo_blocking_country") @@index([isActive], map: "idx_geo_blocking_active") @@ -3804,12 +3819,12 @@ model GeoBlockingRule { // ============================================================================ model PublishedInboxFile { - id Int @id @default(autoincrement()) - originalInboxPath String @map("original_inbox_path") - publishedToPublicMediaId Int @map("published_to_public_media_id") - publishedAt DateTime @map("published_at") - movedToPath String? @map("moved_to_path") - metadataSnapshot Json? @map("metadata_snapshot") + id Int @id @default(autoincrement()) + originalInboxPath String @map("original_inbox_path") + publishedToPublicMediaId Int @map("published_to_public_media_id") + publishedAt DateTime @map("published_at") + movedToPath String? @map("moved_to_path") + metadataSnapshot Json? @map("metadata_snapshot") @@index([originalInboxPath], map: "idx_published_inbox_files_inbox_path") @@index([publishedToPublicMediaId], map: "idx_published_inbox_files_public_media") @@ -3829,7 +3844,7 @@ model VideoOcrResult { createdAt DateTime @default(now()) @map("created_at") // Relations - video Video @relation(fields: [videoId], references: [id]) + video Video @relation(fields: [videoId], references: [id]) @@index([videoId], map: "idx_video_ocr_results_video") @@map("video_ocr_results") @@ -3837,25 +3852,25 @@ model VideoOcrResult { // Enhanced video analytics models model VideoView { - id Int @id @default(autoincrement()) - videoId Int @map("video_id") - userId String? @map("user_id") - ipAddressHash String? @map("ip_address_hash") @db.VarChar(64) // SHA-256 hash - userAgentHash String? @map("user_agent_hash") @db.VarChar(64) // SHA-256 hash - referer String? @db.Text - country String? @db.VarChar(2) // ISO 3166-1 alpha-2 - region String? @db.VarChar(100) - city String? @db.VarChar(100) + id Int @id @default(autoincrement()) + videoId Int @map("video_id") + userId String? @map("user_id") + ipAddressHash String? @map("ip_address_hash") @db.VarChar(64) // SHA-256 hash + userAgentHash String? @map("user_agent_hash") @db.VarChar(64) // SHA-256 hash + referer String? @db.Text + country String? @db.VarChar(2) // ISO 3166-1 alpha-2 + region String? @db.VarChar(100) + city String? @db.VarChar(100) latitude Float? longitude Float? - watchTimeSeconds Int @default(0) @map("watch_time_seconds") - completed Boolean @default(false) - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") + watchTimeSeconds Int @default(0) @map("watch_time_seconds") + completed Boolean @default(false) + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") // Relations - video Video @relation(fields: [videoId], references: [id], onDelete: Cascade) - user User? @relation("VideoViews", fields: [userId], references: [id], onDelete: SetNull) + video Video @relation(fields: [videoId], references: [id], onDelete: Cascade) + user User? @relation("VideoViews", fields: [userId], references: [id], onDelete: SetNull) @@index([videoId], map: "idx_video_views_video") @@index([userId], map: "idx_video_views_user") @@ -3866,15 +3881,15 @@ model VideoView { } model VideoEvent { - id Int @id @default(autoincrement()) - videoId Int @map("video_id") - viewId Int? @map("view_id") - eventType String @map("event_type") @db.VarChar(50) // play, pause, seek, complete - timestamp Decimal @db.Decimal(10, 2) // Video timestamp in seconds - createdAt DateTime @default(now()) @map("created_at") + id Int @id @default(autoincrement()) + videoId Int @map("video_id") + viewId Int? @map("view_id") + eventType String @map("event_type") @db.VarChar(50) // play, pause, seek, complete + timestamp Decimal @db.Decimal(10, 2) // Video timestamp in seconds + createdAt DateTime @default(now()) @map("created_at") // Relations - video Video @relation(fields: [videoId], references: [id], onDelete: Cascade) + video Video @relation(fields: [videoId], references: [id], onDelete: Cascade) @@index([videoId], map: "idx_video_events_video") @@index([viewId], map: "idx_video_events_view") @@ -3895,8 +3910,8 @@ model VideoScheduleHistory { createdAt DateTime @default(now()) @map("created_at") // Relations - video Video @relation(fields: [videoId], references: [id], onDelete: Cascade) - scheduledBy User @relation("VideoScheduleHistory", fields: [scheduledByUserId], references: [id]) + video Video @relation(fields: [videoId], references: [id], onDelete: Cascade) + scheduledBy User @relation("VideoScheduleHistory", fields: [scheduledByUserId], references: [id]) @@index([videoId], map: "idx_video_schedule_history_video") @@index([scheduledFor], map: "idx_video_schedule_history_scheduled") @@ -3911,12 +3926,12 @@ model VideoScheduleHistory { model DocsPageView { id String @id @default(cuid()) - path String // e.g. "/docs/getting-started/" - referrer String? @db.Text // document.referrer - sessionHash String? // anonymous session UUID (sessionStorage) - userAgent String? // for device type breakdown + path String // e.g. "/docs/getting-started/" + referrer String? @db.Text // document.referrer + sessionHash String? // anonymous session UUID (sessionStorage) + userAgent String? // for device type breakdown ipAddressHash String? @map("ip_address_hash") @db.VarChar(64) - country String? @db.VarChar(2) // ISO 3166-1 alpha-2 + country String? @db.VarChar(2) // ISO 3166-1 alpha-2 region String? @db.VarChar(100) city String? @db.VarChar(100) latitude Float? @@ -3934,74 +3949,74 @@ model DocsPageView { // ============================================================================ model Photo { - id Int @id @default(autoincrement()) - path String @unique // Full path to original file - filename String // UUID filename on disk - originalFilename String? @map("original_filename") // Original upload filename - title String? - description String? @db.Text - producer String? - creator String? - tags Json? // String array + id Int @id @default(autoincrement()) + path String @unique // Full path to original file + filename String // UUID filename on disk + originalFilename String? @map("original_filename") // Original upload filename + title String? + description String? @db.Text + producer String? + creator String? + tags Json? // String array // Image metadata (from sharp) - width Int? - height Int? - orientation String? // H / V / S (horizontal/vertical/square) - fileSize BigInt? @map("file_size") - format String? // jpeg, png, webp, avif, gif, tiff, heic - colorSpace String? @map("color_space") // srgb, display-p3, etc. - hasAlpha Boolean? @default(false) @map("has_alpha") - dpi Int? + width Int? + height Int? + orientation String? // H / V / S (horizontal/vertical/square) + fileSize BigInt? @map("file_size") + format String? // jpeg, png, webp, avif, gif, tiff, heic + colorSpace String? @map("color_space") // srgb, display-p3, etc. + hasAlpha Boolean? @default(false) @map("has_alpha") + dpi Int? // EXIF data - cameraMake String? @map("camera_make") - cameraModel String? @map("camera_model") - focalLength String? @map("focal_length") - aperture String? - shutterSpeed String? @map("shutter_speed") - iso Int? - takenAt DateTime? @map("taken_at") - gpsLatitude Float? @map("gps_latitude") @db.Real - gpsLongitude Float? @map("gps_longitude") @db.Real + cameraMake String? @map("camera_make") + cameraModel String? @map("camera_model") + focalLength String? @map("focal_length") + aperture String? + shutterSpeed String? @map("shutter_speed") + iso Int? + takenAt DateTime? @map("taken_at") + gpsLatitude Float? @map("gps_latitude") @db.Real + gpsLongitude Float? @map("gps_longitude") @db.Real // Processed variants - thumbnailPath String? @map("thumbnail_path") - mediumPath String? @map("medium_path") - largePath String? @map("large_path") - webpPath String? @map("webp_path") + thumbnailPath String? @map("thumbnail_path") + mediumPath String? @map("medium_path") + largePath String? @map("large_path") + webpPath String? @map("webp_path") // Publishing (mirrors Video) - isPublished Boolean @default(false) @map("is_published") - publishedAt DateTime? @map("published_at") - category String? - accessLevel String @default("free") @map("access_level") - position Int? @default(0) - isLocked Boolean @default(false) @map("is_locked") + isPublished Boolean @default(false) @map("is_published") + publishedAt DateTime? @map("published_at") + category String? + accessLevel String @default("free") @map("access_level") + position Int? @default(0) + isLocked Boolean @default(false) @map("is_locked") scheduledPublishAt DateTime? @map("scheduled_publish_at") scheduledUnpublishAt DateTime? @map("scheduled_unpublish_at") // Engagement counters - viewCount Int @default(0) @map("view_count") - upvoteCount Int @default(0) @map("upvote_count") - commentCount Int @default(0) @map("comment_count") + viewCount Int @default(0) @map("view_count") + upvoteCount Int @default(0) @map("upvote_count") + commentCount Int @default(0) @map("comment_count") // Album membership - albumId Int? @map("album_id") - albumPosition Int? @default(0) @map("album_position") + albumId Int? @map("album_id") + albumPosition Int? @default(0) @map("album_position") // Tracking - uploaderId String? @map("uploader_id") - createdAt DateTime @default(now()) @map("created_at") + uploaderId String? @map("uploader_id") + createdAt DateTime @default(now()) @map("created_at") // Relations - album PhotoAlbum? @relation("AlbumPhotos", fields: [albumId], references: [id], onDelete: SetNull) - uploader User? @relation("PhotoUploader", fields: [uploaderId], references: [id]) - upvotes PhotoUpvote[] - comments PhotoComment[] - views PhotoView[] - reactions PhotoReaction[] - coverForAlbum PhotoAlbum? @relation("AlbumCover") + album PhotoAlbum? @relation("AlbumPhotos", fields: [albumId], references: [id], onDelete: SetNull) + uploader User? @relation("PhotoUploader", fields: [uploaderId], references: [id]) + upvotes PhotoUpvote[] + comments PhotoComment[] + views PhotoView[] + reactions PhotoReaction[] + coverForAlbum PhotoAlbum? @relation("AlbumCover") @@index([orientation], map: "idx_photos_orientation") @@index([producer], map: "idx_photos_producer") @@ -4014,37 +4029,37 @@ model Photo { } model PhotoAlbum { - id Int @id @default(autoincrement()) - title String - description String? @db.Text - coverPhotoId Int? @unique @map("cover_photo_id") + id Int @id @default(autoincrement()) + title String + description String? @db.Text + coverPhotoId Int? @unique @map("cover_photo_id") // Publishing - isPublished Boolean @default(false) @map("is_published") - publishedAt DateTime? @map("published_at") - category String? - accessLevel String @default("free") @map("access_level") - position Int? @default(0) - isLocked Boolean @default(false) @map("is_locked") + isPublished Boolean @default(false) @map("is_published") + publishedAt DateTime? @map("published_at") + category String? + accessLevel String @default("free") @map("access_level") + position Int? @default(0) + isLocked Boolean @default(false) @map("is_locked") // Aggregate counters - viewCount Int @default(0) @map("view_count") - upvoteCount Int @default(0) @map("upvote_count") - photoCount Int @default(0) @map("photo_count") + viewCount Int @default(0) @map("view_count") + upvoteCount Int @default(0) @map("upvote_count") + photoCount Int @default(0) @map("photo_count") // Scheduling scheduledPublishAt DateTime? @map("scheduled_publish_at") scheduledUnpublishAt DateTime? @map("scheduled_unpublish_at") // Tracking - creatorId String? @map("creator_id") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") + creatorId String? @map("creator_id") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") // Relations - photos Photo[] @relation("AlbumPhotos") - coverPhoto Photo? @relation("AlbumCover", fields: [coverPhotoId], references: [id], onDelete: SetNull) - creator User? @relation("AlbumCreator", fields: [creatorId], references: [id]) + photos Photo[] @relation("AlbumPhotos") + coverPhoto Photo? @relation("AlbumCover", fields: [coverPhotoId], references: [id], onDelete: SetNull) + creator User? @relation("AlbumCreator", fields: [creatorId], references: [id]) @@index([isPublished], map: "idx_photo_albums_published") @@index([creatorId], map: "idx_photo_albums_creator") @@ -4057,8 +4072,8 @@ model PhotoUpvote { sessionId String @map("session_id") createdAt DateTime @default(now()) @map("created_at") - photo Photo @relation(fields: [photoId], references: [id], onDelete: Cascade) - session Session @relation("SessionPhotoUpvotes", fields: [sessionId], references: [id]) + photo Photo @relation(fields: [photoId], references: [id], onDelete: Cascade) + session Session @relation("SessionPhotoUpvotes", fields: [sessionId], references: [id]) @@unique([photoId, sessionId], map: "idx_photo_upvotes_unique") @@index([photoId], map: "idx_photo_upvotes_photo") @@ -4066,18 +4081,18 @@ model PhotoUpvote { } model PhotoComment { - id Int @id @default(autoincrement()) - photoId Int @map("photo_id") - sessionId String @map("session_id") - userId String? @map("user_id") - content String @db.Text - createdAt DateTime @default(now()) @map("created_at") - safetyStatus String @default("approved") @map("safety_status") - isHidden Boolean @default(false) @map("is_hidden") + id Int @id @default(autoincrement()) + photoId Int @map("photo_id") + sessionId String @map("session_id") + userId String? @map("user_id") + content String @db.Text + createdAt DateTime @default(now()) @map("created_at") + safetyStatus String @default("approved") @map("safety_status") + isHidden Boolean @default(false) @map("is_hidden") - photo Photo @relation(fields: [photoId], references: [id], onDelete: Cascade) - session Session @relation("SessionPhotoComments", fields: [sessionId], references: [id]) - user User? @relation("PhotoCommentUser", fields: [userId], references: [id]) + photo Photo @relation(fields: [photoId], references: [id], onDelete: Cascade) + session Session @relation("SessionPhotoComments", fields: [sessionId], references: [id]) + user User? @relation("PhotoCommentUser", fields: [userId], references: [id]) @@index([photoId, createdAt], map: "idx_photo_comments_photo_date") @@index([sessionId], map: "idx_photo_comments_session") @@ -4085,19 +4100,19 @@ model PhotoComment { } model PhotoView { - id Int @id @default(autoincrement()) - photoId Int @map("photo_id") - sessionId String? @map("session_id") - userId String? @map("user_id") - ipAddressHash String? @map("ip_address_hash") - country String? @db.VarChar(2) // ISO 3166-1 alpha-2 - region String? @db.VarChar(100) - city String? @db.VarChar(100) + id Int @id @default(autoincrement()) + photoId Int @map("photo_id") + sessionId String? @map("session_id") + userId String? @map("user_id") + ipAddressHash String? @map("ip_address_hash") + country String? @db.VarChar(2) // ISO 3166-1 alpha-2 + region String? @db.VarChar(100) + city String? @db.VarChar(100) latitude Float? longitude Float? - viewedAt DateTime @default(now()) @map("viewed_at") + viewedAt DateTime @default(now()) @map("viewed_at") - photo Photo @relation(fields: [photoId], references: [id], onDelete: Cascade) + photo Photo @relation(fields: [photoId], references: [id], onDelete: Cascade) @@index([photoId, viewedAt], map: "idx_photo_views_photo_date") @@index([sessionId], map: "idx_photo_views_session") @@ -4106,14 +4121,14 @@ model PhotoView { } model PhotoReaction { - id Int @id @default(autoincrement()) - photoId Int @map("photo_id") - sessionId String @map("session_id") - reactionType String @map("reaction_type") // like, love, laugh, wow, sad, angry - createdAt DateTime @default(now()) @map("created_at") + id Int @id @default(autoincrement()) + photoId Int @map("photo_id") + sessionId String @map("session_id") + reactionType String @map("reaction_type") // like, love, laugh, wow, sad, angry + createdAt DateTime @default(now()) @map("created_at") - photo Photo @relation(fields: [photoId], references: [id], onDelete: Cascade) - session Session @relation("SessionPhotoReactions", fields: [sessionId], references: [id]) + photo Photo @relation(fields: [photoId], references: [id], onDelete: Cascade) + session Session @relation("SessionPhotoReactions", fields: [sessionId], references: [id]) @@unique([photoId, sessionId, reactionType], map: "idx_photo_reactions_unique") @@index([photoId], map: "idx_photo_reactions_photo") @@ -4131,17 +4146,17 @@ enum DocsCommentStatus { } model DocsComment { - id String @id @default(cuid()) - pagePath String - giteaIssueNumber Int - giteaCommentId BigInt - authorName String - authorEmail String? - status DocsCommentStatus @default(PENDING) - reviewedAt DateTime? - reviewedBy String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(cuid()) + pagePath String + giteaIssueNumber Int + giteaCommentId BigInt + authorName String + authorEmail String? + status DocsCommentStatus @default(PENDING) + reviewedAt DateTime? + reviewedBy String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt @@index([pagePath, status]) @@index([status, createdAt]) @@ -4203,8 +4218,8 @@ model SmsContactList { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - entries SmsContactListEntry[] - campaigns SmsCampaign[] + entries SmsContactListEntry[] + campaigns SmsCampaign[] @@map("sms_contact_lists") } @@ -4216,7 +4231,7 @@ model SmsContactListEntry { phone String name String? email String? - customFields Json? // Arbitrary key-value pairs from CSV columns + customFields Json? // Arbitrary key-value pairs from CSV columns createdAt DateTime @default(now()) @@unique([listId, phone]) @@ -4226,31 +4241,31 @@ model SmsContactListEntry { } model SmsCampaign { - id String @id @default(cuid()) - name String - messageTemplate String @db.Text - status SmsCampaignStatus @default(DRAFT) - totalRecipients Int @default(0) - totalSent Int @default(0) - totalFailed Int @default(0) - totalResponded Int @default(0) - delayBetweenMs Int @default(3000) - startedAt DateTime? - completedAt DateTime? + id String @id @default(cuid()) + name String + messageTemplate String @db.Text + status SmsCampaignStatus @default(DRAFT) + totalRecipients Int @default(0) + totalSent Int @default(0) + totalFailed Int @default(0) + totalResponded Int @default(0) + delayBetweenMs Int @default(3000) + startedAt DateTime? + completedAt DateTime? // Relations - contactListId String - contactList SmsContactList @relation(fields: [contactListId], references: [id]) - advocacyCampaignId String? - advocacyCampaign Campaign? @relation("SmsCampaigns", fields: [advocacyCampaignId], references: [id], onDelete: SetNull) - createdByUserId String? - createdByUser User? @relation("SmsCampaignCreator", fields: [createdByUserId], references: [id], onDelete: SetNull) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + contactListId String + contactList SmsContactList @relation(fields: [contactListId], references: [id]) + advocacyCampaignId String? + advocacyCampaign Campaign? @relation("SmsCampaigns", fields: [advocacyCampaignId], references: [id], onDelete: SetNull) + createdByUserId String? + createdByUser User? @relation("SmsCampaignCreator", fields: [createdByUserId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt - recipients SmsCampaignRecipient[] - messages SmsMessage[] - conversations SmsConversation[] + recipients SmsCampaignRecipient[] + messages SmsMessage[] + conversations SmsConversation[] @@index([status]) @@index([contactListId]) @@ -4280,18 +4295,18 @@ model SmsMessage { message String @db.Text direction SmsMessageDirection status SmsMessageStatus @default(PENDING) - connectionType String? // e.g. "termux" + connectionType String? // e.g. "termux" // Campaign context (nullable for ad-hoc messages) campaignId String? - campaign SmsCampaign? @relation(fields: [campaignId], references: [id], onDelete: SetNull) + campaign SmsCampaign? @relation(fields: [campaignId], references: [id], onDelete: SetNull) conversationId String? - conversation SmsConversation? @relation(fields: [conversationId], references: [id], onDelete: SetNull) + conversation SmsConversation? @relation(fields: [conversationId], references: [id], onDelete: SetNull) // Response classification (for inbound messages) - responseType SmsResponseType? - isRead Boolean @default(false) - sentAt DateTime @default(now()) + responseType SmsResponseType? + isRead Boolean @default(false) + sentAt DateTime @default(now()) @@index([phone]) @@index([campaignId]) @@ -4301,25 +4316,25 @@ model SmsMessage { } model SmsConversation { - id String @id @default(cuid()) + id String @id @default(cuid()) phone String contactName String? campaignId String? - campaign SmsCampaign? @relation(fields: [campaignId], references: [id], onDelete: SetNull) + campaign SmsCampaign? @relation(fields: [campaignId], references: [id], onDelete: SetNull) contactId String? - contact Contact? @relation("ContactSmsConversations", fields: [contactId], references: [id], onDelete: SetNull) - status SmsConversationStatus @default(ACTIVE) - totalMessages Int @default(0) - totalResponses Int @default(0) - unreadCount Int @default(0) + contact Contact? @relation("ContactSmsConversations", fields: [contactId], references: [id], onDelete: SetNull) + status SmsConversationStatus @default(ACTIVE) + totalMessages Int @default(0) + totalResponses Int @default(0) + unreadCount Int @default(0) lastMessageAt DateTime? lastResponseAt DateTime? - notes String? @db.Text - tags Json? // String array - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + notes String? @db.Text + tags Json? // String array + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt - messages SmsMessage[] + messages SmsMessage[] @@unique([phone, campaignId]) @@index([status]) @@ -4400,57 +4415,57 @@ enum ContactActivityType { } model Contact { - id String @id @default(cuid()) - displayName String - firstName String? - lastName String? - email String? - phone String? - pronouns String? + id String @id @default(cuid()) + displayName String + firstName String? + lastName String? + email String? + phone String? + pronouns String? // CRM data - tags Json @default("[]") // String array - notes String? @db.Text - supportLevel SupportLevel? - signRequested Boolean @default(false) + tags Json @default("[]") // String array + notes String? @db.Text + supportLevel SupportLevel? + signRequested Boolean @default(false) // Consent - emailOptOut Boolean @default(false) - smsOptOut Boolean @default(false) - doNotContact Boolean @default(false) + emailOptOut Boolean @default(false) + smsOptOut Boolean @default(false) + doNotContact Boolean @default(false) // Self-service profile - profileToken String? @unique // Random hex token for public access - profileTokenExpiresAt DateTime? // null = never expires - profilePasswordHash String? // bcrypt hash; null = no password - coverPhotoPath String? // Path to processed cover photo on disk - lastSelfEditAt DateTime? // Track last self-service edit + profileToken String? @unique // Random hex token for public access + profileTokenExpiresAt DateTime? // null = never expires + profilePasswordHash String? // bcrypt hash; null = no password + coverPhotoPath String? // Path to processed cover photo on disk + lastSelfEditAt DateTime? // Track last self-service edit // Source tracking - primarySource ContactSource @default(MANUAL) + primarySource ContactSource @default(MANUAL) // Links to existing models - userId String? @unique - user User? @relation("UserContact", fields: [userId], references: [id], onDelete: SetNull) + userId String? @unique + user User? @relation("UserContact", fields: [userId], references: [id], onDelete: SetNull) // Merge support - mergedIntoId String? - mergedInto Contact? @relation("ContactMerge", fields: [mergedIntoId], references: [id], onDelete: SetNull) - mergedContacts Contact[] @relation("ContactMerge") + mergedIntoId String? + mergedInto Contact? @relation("ContactMerge", fields: [mergedIntoId], references: [id], onDelete: SetNull) + mergedContacts Contact[] @relation("ContactMerge") // Audit createdByUserId String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt // Relations addresses ContactAddress[] emails ContactEmail[] phones ContactPhone[] - connectionsFrom ContactConnection[] @relation("ConnectionFrom") - connectionsTo ContactConnection[] @relation("ConnectionTo") + connectionsFrom ContactConnection[] @relation("ConnectionFrom") + connectionsTo ContactConnection[] @relation("ConnectionTo") activities ContactActivity[] - smsConversations SmsConversation[] @relation("ContactSmsConversations") + smsConversations SmsConversation[] @relation("ContactSmsConversations") pollVotes SchedulingPollVote[] @relation("PollVoteContact") strawPollVotes StrawPollVote[] @relation("StrawPollVoteContact") participantNeeds ParticipantNeeds? @relation("ContactParticipantNeeds") @@ -4465,29 +4480,29 @@ model Contact { } model CrmTag { - id String @id @default(cuid()) - name String @unique - slug String @unique + id String @id @default(cuid()) + name String @unique + slug String @unique description String? - color String? // Hex, e.g. "#1890ff" - listmonkListId Int? // Corresponding Listmonk list ID - contactCount Int @default(0) // Denormalized count + color String? // Hex, e.g. "#1890ff" + listmonkListId Int? // Corresponding Listmonk list ID + contactCount Int @default(0) // Denormalized count createdByUserId String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt @@index([name]) @@map("crm_tags") } model ContactAddress { - id String @id @default(cuid()) - contactId String - contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade) - addressId String - address Address @relation(fields: [addressId], references: [id], onDelete: Cascade) - isPrimary Boolean @default(false) - createdAt DateTime @default(now()) + id String @id @default(cuid()) + contactId String + contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade) + addressId String + address Address @relation(fields: [addressId], references: [id], onDelete: Cascade) + isPrimary Boolean @default(false) + createdAt DateTime @default(now()) @@unique([contactId, addressId]) @@index([addressId]) @@ -4495,13 +4510,13 @@ model ContactAddress { } model ContactEmail { - id String @id @default(cuid()) - contactId String - contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade) - email String - label String? // "Personal", "Work", "Campaign", etc. - isPrimary Boolean @default(false) - createdAt DateTime @default(now()) + id String @id @default(cuid()) + contactId String + contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade) + email String + label String? // "Personal", "Work", "Campaign", etc. + isPrimary Boolean @default(false) + createdAt DateTime @default(now()) @@unique([contactId, email]) @@index([email]) @@ -4510,13 +4525,13 @@ model ContactEmail { } model ContactPhone { - id String @id @default(cuid()) - contactId String - contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade) - phone String - label String? // "Mobile", "Home", "Work", etc. - isPrimary Boolean @default(false) - createdAt DateTime @default(now()) + id String @id @default(cuid()) + contactId String + contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade) + phone String + label String? // "Mobile", "Home", "Work", etc. + isPrimary Boolean @default(false) + createdAt DateTime @default(now()) @@unique([contactId, phone]) @@index([phone]) @@ -4575,9 +4590,9 @@ model Meeting { endTime DateTime? @map("end_time") // Reverse relations (one-to-one) - shift Shift? @relation("ShiftMeeting") - group SocialGroup? @relation("GroupMeeting") - ticketedEvent TicketedEvent? @relation("EventMeeting") + shift Shift? @relation("ShiftMeeting") + group SocialGroup? @relation("GroupMeeting") + ticketedEvent TicketedEvent? @relation("EventMeeting") @@map("meetings") } @@ -4600,40 +4615,40 @@ enum PollVoteValue { } model SchedulingPoll { - id String @id @default(cuid()) - slug String @unique - title String - description String? @db.Text - location String? - status SchedulingPollStatus @default(OPEN) - timezone String @default("America/Edmonton") - finalizedOptionId String? @unique @map("finalized_option_id") - finalizedOption SchedulingPollOption? @relation("FinalizedOption", fields: [finalizedOptionId], references: [id], onDelete: SetNull) - 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") - createdByUserId String @map("created_by_user_id") - createdBy User @relation("PollCreator", fields: [createdByUserId], references: [id]) - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") + id String @id @default(cuid()) + slug String @unique + title String + description String? @db.Text + location String? + status SchedulingPollStatus @default(OPEN) + timezone String @default("America/Edmonton") + finalizedOptionId String? @unique @map("finalized_option_id") + finalizedOption SchedulingPollOption? @relation("FinalizedOption", fields: [finalizedOptionId], references: [id], onDelete: SetNull) + 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") + createdByUserId String @map("created_by_user_id") + createdBy User @relation("PollCreator", fields: [createdByUserId], references: [id]) + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") - options SchedulingPollOption[] @relation("PollOptions") - votes SchedulingPollVote[] @relation("PollVotes") - comments SchedulingPollComment[] @relation("PollComments") - agenda MeetingAgenda? @relation("PollAgenda") + options SchedulingPollOption[] @relation("PollOptions") + votes SchedulingPollVote[] @relation("PollVotes") + comments SchedulingPollComment[] @relation("PollComments") + agenda MeetingAgenda? @relation("PollAgenda") @@index([createdByUserId]) @@index([status]) @@ -4641,15 +4656,15 @@ model SchedulingPoll { } model SchedulingPollOption { - id String @id @default(cuid()) - pollId String @map("poll_id") + id String @id @default(cuid()) + pollId String @map("poll_id") poll SchedulingPoll @relation("PollOptions", fields: [pollId], references: [id], onDelete: Cascade) - date DateTime @db.Date - startTime String @map("start_time") // HH:MM - endTime String @map("end_time") // HH:MM - sortOrder Int @default(0) @map("sort_order") + date DateTime @db.Date + startTime String @map("start_time") // HH:MM + endTime String @map("end_time") // HH:MM + sortOrder Int @default(0) @map("sort_order") - votes SchedulingPollVote[] @relation("OptionVotes") + votes SchedulingPollVote[] @relation("OptionVotes") // Reverse 1:1 for finalized option finalizedForPoll SchedulingPoll? @relation("FinalizedOption") @@ -4659,21 +4674,21 @@ model SchedulingPollOption { } model SchedulingPollVote { - id String @id @default(cuid()) - pollId String @map("poll_id") - poll SchedulingPoll @relation("PollVotes", fields: [pollId], references: [id], onDelete: Cascade) - optionId String @map("option_id") + id String @id @default(cuid()) + pollId String @map("poll_id") + poll SchedulingPoll @relation("PollVotes", fields: [pollId], references: [id], onDelete: Cascade) + optionId String @map("option_id") option SchedulingPollOption @relation("OptionVotes", fields: [optionId], references: [id], onDelete: Cascade) - userId String? @map("user_id") - user User? @relation("PollVoter", fields: [userId], references: [id], onDelete: SetNull) - 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) + userId String? @map("user_id") + user User? @relation("PollVoter", fields: [userId], references: [id], onDelete: SetNull) + 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") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") @@unique([optionId, userId]) @@unique([optionId, voterToken]) @@ -4683,14 +4698,14 @@ model SchedulingPollVote { } model SchedulingPollComment { - id String @id @default(cuid()) - pollId String @map("poll_id") + id String @id @default(cuid()) + pollId String @map("poll_id") poll SchedulingPoll @relation("PollComments", fields: [pollId], references: [id], onDelete: Cascade) - userId String? @map("user_id") - user User? @relation("PollCommenter", fields: [userId], references: [id], onDelete: SetNull) - authorName String @map("author_name") - content String @db.Text - createdAt DateTime @default(now()) @map("created_at") + userId String? @map("user_id") + user User? @relation("PollCommenter", fields: [userId], references: [id], onDelete: SetNull) + authorName String @map("author_name") + content String @db.Text + createdAt DateTime @default(now()) @map("created_at") @@index([pollId]) @@map("scheduling_poll_comments") @@ -4712,8 +4727,8 @@ model InviteCode { createdAt DateTime @default(now()) @map("created_at") // Relations - createdBy User @relation("InviteCodesCreated", fields: [createdByUserId], references: [id]) - referrals Referral[] @relation("InviteCodeReferrals") + createdBy User @relation("InviteCodesCreated", fields: [createdByUserId], references: [id]) + referrals Referral[] @relation("InviteCodeReferrals") @@index([code], map: "idx_invite_codes_code") @@index([createdByUserId], map: "idx_invite_codes_created_by") @@ -4721,17 +4736,17 @@ model InviteCode { } model Referral { - id Int @id @default(autoincrement()) - referrerId String @map("referrer_id") - referredUserId String @unique @map("referred_user_id") - inviteCodeId String? @map("invite_code_id") - referralSource String? @map("referral_source") - completedAt DateTime @default(now()) @map("completed_at") + id Int @id @default(autoincrement()) + referrerId String @map("referrer_id") + referredUserId String @unique @map("referred_user_id") + inviteCodeId String? @map("invite_code_id") + referralSource String? @map("referral_source") + completedAt DateTime @default(now()) @map("completed_at") // Relations - referrer User @relation("ReferralsMade", fields: [referrerId], references: [id]) - referredUser User @relation("ReferredBy", fields: [referredUserId], references: [id]) - inviteCode InviteCode? @relation("InviteCodeReferrals", fields: [inviteCodeId], references: [id]) + referrer User @relation("ReferralsMade", fields: [referrerId], references: [id]) + referredUser User @relation("ReferredBy", fields: [referredUserId], references: [id]) + inviteCode InviteCode? @relation("InviteCodeReferrals", fields: [inviteCodeId], references: [id]) @@index([referrerId], map: "idx_referrals_referrer") @@map("referrals") @@ -4755,23 +4770,23 @@ enum ImpactStoryStatus { } model ImpactStory { - id String @id @default(cuid()) - campaignId String @map("campaign_id") + id String @id @default(cuid()) + campaignId String @map("campaign_id") type ImpactStoryType status ImpactStoryStatus @default(DRAFT) title String - body String @db.Text - coverImageUrl String? @map("cover_image_url") - milestoneValue Int? @map("milestone_value") - milestoneMetric String? @map("milestone_metric") - createdByUserId String? @map("created_by_user_id") - publishedAt DateTime? @map("published_at") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") + body String @db.Text + coverImageUrl String? @map("cover_image_url") + milestoneValue Int? @map("milestone_value") + milestoneMetric String? @map("milestone_metric") + createdByUserId String? @map("created_by_user_id") + publishedAt DateTime? @map("published_at") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") // Relations - campaign Campaign @relation("CampaignStories", fields: [campaignId], references: [id]) - createdBy User? @relation("ImpactStoryCreator", fields: [createdByUserId], references: [id]) + campaign Campaign @relation("CampaignStories", fields: [campaignId], references: [id]) + createdBy User? @relation("ImpactStoryCreator", fields: [createdByUserId], references: [id]) @@index([campaignId], map: "idx_impact_stories_campaign") @@index([status], map: "idx_impact_stories_status") @@ -4782,13 +4797,13 @@ model ImpactStory { model CampaignMilestone { id Int @id @default(autoincrement()) campaignId String @map("campaign_id") - metric String // "emails_sent", "verified_responses" + metric String // "emails_sent", "verified_responses" threshold Int reachedAt DateTime @default(now()) @map("reached_at") storyGenerated Boolean @default(false) @map("story_generated") // Relations - campaign Campaign @relation("CampaignMilestones", fields: [campaignId], references: [id]) + campaign Campaign @relation("CampaignMilestones", fields: [campaignId], references: [id]) @@unique([campaignId, metric, threshold]) @@map("campaign_milestones") @@ -4819,9 +4834,9 @@ model VolunteerSpotlight { updatedAt DateTime @updatedAt @map("updated_at") // Relations - user User @relation("SpotlightUser", fields: [userId], references: [id]) - nominatedBy User? @relation("SpotlightNominator", fields: [nominatedByUserId], references: [id]) - approvedBy User? @relation("SpotlightApprover", fields: [approvedByUserId], references: [id]) + user User @relation("SpotlightUser", fields: [userId], references: [id]) + nominatedBy User? @relation("SpotlightNominator", fields: [nominatedByUserId], references: [id]) + approvedBy User? @relation("SpotlightApprover", fields: [approvedByUserId], references: [id]) @@index([userId], map: "idx_volunteer_spotlights_user") @@index([status], map: "idx_volunteer_spotlights_status") @@ -4865,8 +4880,8 @@ model Challenge { updatedAt DateTime @updatedAt @map("updated_at") // Relations - createdBy User @relation("ChallengesCreated", fields: [createdByUserId], references: [id]) - teams ChallengeTeam[] @relation("ChallengeTeams") + createdBy User @relation("ChallengesCreated", fields: [createdByUserId], references: [id]) + teams ChallengeTeam[] @relation("ChallengeTeams") @@index([status], map: "idx_challenges_status") @@index([startsAt], map: "idx_challenges_starts_at") @@ -4874,18 +4889,18 @@ model Challenge { } model ChallengeTeam { - id String @id @default(cuid()) - challengeId String @map("challenge_id") - name String - captainUserId String @map("captain_user_id") - score Int @default(0) - lastScoredAt DateTime? @map("last_scored_at") - createdAt DateTime @default(now()) @map("created_at") + id String @id @default(cuid()) + challengeId String @map("challenge_id") + name String + captainUserId String @map("captain_user_id") + score Int @default(0) + lastScoredAt DateTime? @map("last_scored_at") + createdAt DateTime @default(now()) @map("created_at") // Relations - challenge Challenge @relation("ChallengeTeams", fields: [challengeId], references: [id]) - captain User @relation("ChallengeTeamsCaptained", fields: [captainUserId], references: [id]) - members ChallengeTeamMember[] @relation("ChallengeTeamMembers") + challenge Challenge @relation("ChallengeTeams", fields: [challengeId], references: [id]) + captain User @relation("ChallengeTeamsCaptained", fields: [captainUserId], references: [id]) + members ChallengeTeamMember[] @relation("ChallengeTeamMembers") @@unique([challengeId, name]) @@index([challengeId], map: "idx_challenge_teams_challenge") @@ -4894,15 +4909,15 @@ model ChallengeTeam { } model ChallengeTeamMember { - id Int @id @default(autoincrement()) - teamId String @map("team_id") - userId String @map("user_id") - score Int @default(0) - joinedAt DateTime @default(now()) @map("joined_at") + id Int @id @default(autoincrement()) + teamId String @map("team_id") + userId String @map("user_id") + score Int @default(0) + joinedAt DateTime @default(now()) @map("joined_at") // Relations - team ChallengeTeam @relation("ChallengeTeamMembers", fields: [teamId], references: [id], onDelete: Cascade) - user User @relation("ChallengeParticipations", fields: [userId], references: [id]) + team ChallengeTeam @relation("ChallengeTeamMembers", fields: [teamId], references: [id], onDelete: Cascade) + user User @relation("ChallengeParticipations", fields: [userId], references: [id]) @@unique([teamId, userId]) @@index([userId], map: "idx_challenge_team_members_user") @@ -4947,57 +4962,58 @@ enum EventFormat { } model TicketedEvent { - id String @id @default(cuid()) - slug String @unique - title String - description String? @db.Text - richDescription String? @db.Text @map("rich_description") + id String @id @default(cuid()) + slug String @unique + title String + description String? @db.Text + richDescription String? @map("rich_description") @db.Text // Schedule - date DateTime @db.Date - startTime String @map("start_time") - endTime String @map("end_time") - doorsOpenTime String? @map("doors_open_time") + date DateTime @db.Date + startTime String @map("start_time") + endTime String @map("end_time") + doorsOpenTime String? @map("doors_open_time") // Venue - venueName String? @map("venue_name") - venueAddress String? @map("venue_address") - latitude Decimal? @db.Decimal(10, 7) - longitude Decimal? @db.Decimal(10, 7) + venueName String? @map("venue_name") + venueAddress String? @map("venue_address") + latitude Decimal? @db.Decimal(10, 7) + longitude Decimal? @db.Decimal(10, 7) // Status - status TicketedEventStatus @default(DRAFT) - visibility TicketedEventVisibility @default(PUBLIC) - inviteCode String? @unique @map("invite_code") + status TicketedEventStatus @default(DRAFT) + visibility TicketedEventVisibility @default(PUBLIC) + inviteCode String? @unique @map("invite_code") + featured Boolean @default(false) // Media - coverImageUrl String? @map("cover_image_url") + coverImageUrl String? @map("cover_image_url") // Capacity - maxAttendees Int? @map("max_attendees") - currentAttendees Int @default(0) @map("current_attendees") + maxAttendees Int? @map("max_attendees") + currentAttendees Int @default(0) @map("current_attendees") // Gancio sync - gancioEventId Int? @map("gancio_event_id") + gancioEventId Int? @map("gancio_event_id") // Format & Meeting - eventFormat EventFormat @default(IN_PERSON) @map("event_format") - meetingId String? @unique @map("meeting_id") + eventFormat EventFormat @default(IN_PERSON) @map("event_format") + meetingId String? @unique @map("meeting_id") // Creator - createdByUserId String @map("created_by_user_id") - organizerName String? @map("organizer_name") - organizerEmail String? @map("organizer_email") + createdByUserId String @map("created_by_user_id") + organizerName String? @map("organizer_name") + organizerEmail String? @map("organizer_email") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") // Relations - createdBy User @relation("EventCreator", fields: [createdByUserId], references: [id]) - meeting Meeting? @relation("EventMeeting", fields: [meetingId], references: [id], onDelete: SetNull) - ticketTiers TicketTier[] @relation("EventTiers") - tickets Ticket[] @relation("EventTickets") - checkIns CheckIn[] @relation("EventCheckIns") + createdBy User @relation("EventCreator", fields: [createdByUserId], references: [id]) + meeting Meeting? @relation("EventMeeting", fields: [meetingId], references: [id], onDelete: SetNull) + ticketTiers TicketTier[] @relation("EventTiers") + tickets Ticket[] @relation("EventTickets") + checkIns CheckIn[] @relation("EventCheckIns") @@index([status], map: "idx_ticketed_events_status") @@index([date], map: "idx_ticketed_events_date") @@ -5007,55 +5023,55 @@ model TicketedEvent { } model TicketTier { - id String @id @default(cuid()) - eventId String @map("event_id") - name String - description String? - tierType TicketTierType @map("tier_type") - priceCAD Int @default(0) @map("price_cad") // In cents - minDonationCAD Int? @map("min_donation_cad") // In cents - maxQuantity Int? @map("max_quantity") - soldCount Int @default(0) @map("sold_count") - reservedCount Int @default(0) @map("reserved_count") // Pending checkout sessions - maxPerOrder Int @default(10) @map("max_per_order") - salesStartAt DateTime? @map("sales_start_at") - salesEndAt DateTime? @map("sales_end_at") - sortOrder Int @default(0) @map("sort_order") - isActive Boolean @default(true) @map("is_active") + id String @id @default(cuid()) + eventId String @map("event_id") + name String + description String? + tierType TicketTierType @map("tier_type") + priceCAD Int @default(0) @map("price_cad") // In cents + minDonationCAD Int? @map("min_donation_cad") // In cents + maxQuantity Int? @map("max_quantity") + soldCount Int @default(0) @map("sold_count") + reservedCount Int @default(0) @map("reserved_count") // Pending checkout sessions + maxPerOrder Int @default(10) @map("max_per_order") + salesStartAt DateTime? @map("sales_start_at") + salesEndAt DateTime? @map("sales_end_at") + sortOrder Int @default(0) @map("sort_order") + isActive Boolean @default(true) @map("is_active") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") // Relations - event TicketedEvent @relation("EventTiers", fields: [eventId], references: [id], onDelete: Cascade) - tickets Ticket[] @relation("TierTickets") + event TicketedEvent @relation("EventTiers", fields: [eventId], references: [id], onDelete: Cascade) + tickets Ticket[] @relation("TierTickets") @@index([eventId], map: "idx_ticket_tiers_event") @@map("ticket_tiers") } model Ticket { - id String @id @default(cuid()) - ticketCode String @unique @map("ticket_code") - tokenHash String @unique @map("token_hash") - eventId String @map("event_id") - tierId String @map("tier_id") - orderId String? @map("order_id") - holderEmail String @map("holder_email") - holderName String? @map("holder_name") - userId String? @map("user_id") - status TicketStatus @default(VALID) - checkedInAt DateTime? @map("checked_in_at") - checkedInByUserId String? @map("checked_in_by_user_id") - issuedAt DateTime @default(now()) @map("issued_at") - updatedAt DateTime @updatedAt @map("updated_at") + id String @id @default(cuid()) + ticketCode String @unique @map("ticket_code") + tokenHash String @unique @map("token_hash") + eventId String @map("event_id") + tierId String @map("tier_id") + orderId String? @map("order_id") + holderEmail String @map("holder_email") + holderName String? @map("holder_name") + userId String? @map("user_id") + status TicketStatus @default(VALID) + checkedInAt DateTime? @map("checked_in_at") + checkedInByUserId String? @map("checked_in_by_user_id") + issuedAt DateTime @default(now()) @map("issued_at") + updatedAt DateTime @updatedAt @map("updated_at") // Relations - event TicketedEvent @relation("EventTickets", fields: [eventId], references: [id]) - tier TicketTier @relation("TierTickets", fields: [tierId], references: [id]) - order Order? @relation("TicketOrder", fields: [orderId], references: [id]) - holder User? @relation("TicketHolder", fields: [userId], references: [id]) - checkIns CheckIn[] @relation("TicketCheckIns") + event TicketedEvent @relation("EventTickets", fields: [eventId], references: [id]) + tier TicketTier @relation("TierTickets", fields: [tierId], references: [id]) + order Order? @relation("TicketOrder", fields: [orderId], references: [id]) + holder User? @relation("TicketHolder", fields: [userId], references: [id]) + checkIns CheckIn[] @relation("TicketCheckIns") @@index([eventId], map: "idx_tickets_event") @@index([tierId], map: "idx_tickets_tier") @@ -5066,18 +5082,18 @@ model Ticket { } model CheckIn { - id String @id @default(cuid()) - ticketId String @map("ticket_id") - eventId String @map("event_id") - checkedInByUserId String? @map("checked_in_by_user_id") - method String // "QR" | "MANUAL" | "CODE" - checkedInAt DateTime @default(now()) @map("checked_in_at") + id String @id @default(cuid()) + ticketId String @map("ticket_id") + eventId String @map("event_id") + checkedInByUserId String? @map("checked_in_by_user_id") + method String // "QR" | "MANUAL" | "CODE" + checkedInAt DateTime @default(now()) @map("checked_in_at") notes String? // Relations - ticket Ticket @relation("TicketCheckIns", fields: [ticketId], references: [id]) - event TicketedEvent @relation("EventCheckIns", fields: [eventId], references: [id]) - checkedInBy User? @relation("CheckInUser", fields: [checkedInByUserId], references: [id]) + ticket Ticket @relation("TicketCheckIns", fields: [ticketId], references: [id]) + event TicketedEvent @relation("EventCheckIns", fields: [eventId], references: [id]) + checkedInBy User? @relation("CheckInUser", fields: [checkedInByUserId], references: [id]) @@index([eventId], map: "idx_checkins_event") @@index([ticketId], map: "idx_checkins_ticket") @@ -5171,22 +5187,22 @@ enum SharedViewMemberStatus { } model CalendarLayer { - id String @id @default(cuid()) - userId String @map("user_id") - name String - layerType CalendarLayerType @map("layer_type") - systemType CalendarSystemType? @map("system_type") - color String @default("#1890ff") - visibility CalendarVisibility @default(PRIVATE) - isEnabled Boolean @default(true) @map("is_enabled") - sortOrder Int @default(0) @map("sort_order") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") + id String @id @default(cuid()) + userId String @map("user_id") + name String + layerType CalendarLayerType @map("layer_type") + systemType CalendarSystemType? @map("system_type") + color String @default("#1890ff") + visibility CalendarVisibility @default(PRIVATE) + isEnabled Boolean @default(true) @map("is_enabled") + sortOrder Int @default(0) @map("sort_order") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") // Relations - user User @relation("CalendarLayerOwner", fields: [userId], references: [id], onDelete: Cascade) - items CalendarItem[] @relation("CalendarLayerItems") - feed CalendarFeed? @relation("CalendarFeedLayer") + user User @relation("CalendarLayerOwner", fields: [userId], references: [id], onDelete: Cascade) + items CalendarItem[] @relation("CalendarLayerItems") + feed CalendarFeed? @relation("CalendarFeedLayer") @@unique([userId, systemType], map: "idx_calendar_layers_user_system") @@index([userId], map: "idx_calendar_layers_user") @@ -5194,38 +5210,38 @@ model CalendarLayer { } model CalendarItem { - id String @id @default(cuid()) - userId String @map("user_id") - layerId String @map("layer_id") - title String - description String? @db.Text - date DateTime @db.Date - startTime String @map("start_time") // HH:MM - endTime String @map("end_time") // HH:MM - isAllDay Boolean @default(false) @map("is_all_day") - itemType CalendarItemType @default(EVENT) @map("item_type") - location String? - color String? - visibility CalendarVisibility? // null = inherit from layer - busyStatus CalendarBusyStatus @default(BUSY) @map("busy_status") - showDetailsTo CalendarShowDetailsTo @default(FRIENDS) @map("show_details_to") + id String @id @default(cuid()) + userId String @map("user_id") + layerId String @map("layer_id") + title String + description String? @db.Text + date DateTime @db.Date + startTime String @map("start_time") // HH:MM + endTime String @map("end_time") // HH:MM + isAllDay Boolean @default(false) @map("is_all_day") + itemType CalendarItemType @default(EVENT) @map("item_type") + location String? + color String? + visibility CalendarVisibility? // null = inherit from layer + busyStatus CalendarBusyStatus @default(BUSY) @map("busy_status") + showDetailsTo CalendarShowDetailsTo @default(FRIENDS) @map("show_details_to") // Recurrence - recurrenceRule Json? @map("recurrence_rule") - recurrenceEnd DateTime? @map("recurrence_end") - seriesId String? @map("series_id") - isException Boolean @default(false) @map("is_exception") + recurrenceRule Json? @map("recurrence_rule") + recurrenceEnd DateTime? @map("recurrence_end") + seriesId String? @map("series_id") + isException Boolean @default(false) @map("is_exception") // Source tracking - sourceType CalendarItemSource @default(MANUAL) @map("source_type") - sourceId String? @map("source_id") + sourceType CalendarItemSource @default(MANUAL) @map("source_type") + sourceId String? @map("source_id") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") // Relations - user User @relation("CalendarItemOwner", fields: [userId], references: [id], onDelete: Cascade) - layer CalendarLayer @relation("CalendarLayerItems", fields: [layerId], references: [id], onDelete: Cascade) + user User @relation("CalendarItemOwner", fields: [userId], references: [id], onDelete: Cascade) + layer CalendarLayer @relation("CalendarLayerItems", fields: [layerId], references: [id], onDelete: Cascade) @@index([userId, date], map: "idx_calendar_items_user_date") @@index([layerId, date], map: "idx_calendar_items_layer_date") @@ -5235,61 +5251,61 @@ model CalendarItem { } model CalendarFeed { - id String @id @default(cuid()) - userId String @map("user_id") + id String @id @default(cuid()) + userId String @map("user_id") name String url String - layerId String @unique @map("layer_id") + layerId String @unique @map("layer_id") refreshInterval CalendarFeedInterval @default(HOURLY) @map("refresh_interval") - lastFetchedAt DateTime? @map("last_fetched_at") - lastStatus CalendarFeedStatus @default(PENDING) @map("last_status") - lastError String? @map("last_error") - itemCount Int @default(0) @map("item_count") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") + lastFetchedAt DateTime? @map("last_fetched_at") + lastStatus CalendarFeedStatus @default(PENDING) @map("last_status") + lastError String? @map("last_error") + itemCount Int @default(0) @map("item_count") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") // Relations - user User @relation("CalendarFeedOwner", fields: [userId], references: [id], onDelete: Cascade) - layer CalendarLayer @relation("CalendarFeedLayer", fields: [layerId], references: [id], onDelete: Cascade) + user User @relation("CalendarFeedOwner", fields: [userId], references: [id], onDelete: Cascade) + layer CalendarLayer @relation("CalendarFeedLayer", fields: [layerId], references: [id], onDelete: Cascade) @@index([userId], map: "idx_calendar_feeds_user") @@map("calendar_feeds") } model SharedCalendarView { - id String @id @default(cuid()) + id String @id @default(cuid()) name String - description String? @db.Text - ownerId String @map("owner_id") - viewType SharedViewType @default(MANUAL) @map("view_type") - autoIncludeRoles Json? @map("auto_include_roles") - includedLayerTypes Json @default("[]") @map("included_layer_types") - shareScope SharedViewScope @default(MEMBERS) @map("share_scope") - shareToken String? @unique @map("share_token") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") + description String? @db.Text + ownerId String @map("owner_id") + viewType SharedViewType @default(MANUAL) @map("view_type") + autoIncludeRoles Json? @map("auto_include_roles") + includedLayerTypes Json @default("[]") @map("included_layer_types") + shareScope SharedViewScope @default(MEMBERS) @map("share_scope") + shareToken String? @unique @map("share_token") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") // Relations - owner User @relation("SharedViewOwner", fields: [ownerId], references: [id], onDelete: Cascade) - members SharedCalendarMember[] @relation("SharedViewMembers") - comments SharedViewComment[] @relation("SharedViewComments") - reactions SharedViewReaction[] @relation("SharedViewReactions") + owner User @relation("SharedViewOwner", fields: [ownerId], references: [id], onDelete: Cascade) + members SharedCalendarMember[] @relation("SharedViewMembers") + comments SharedViewComment[] @relation("SharedViewComments") + reactions SharedViewReaction[] @relation("SharedViewReactions") @@index([ownerId], map: "idx_shared_views_owner") @@map("shared_calendar_views") } model SharedCalendarMember { - id String @id @default(cuid()) - viewId String @map("view_id") - userId String @map("user_id") - status SharedViewMemberStatus @default(INVITED) - color String @default("#1890ff") - joinedAt DateTime? @map("joined_at") + id String @id @default(cuid()) + viewId String @map("view_id") + userId String @map("user_id") + status SharedViewMemberStatus @default(INVITED) + color String @default("#1890ff") + joinedAt DateTime? @map("joined_at") // Relations - view SharedCalendarView @relation("SharedViewMembers", fields: [viewId], references: [id], onDelete: Cascade) - user User @relation("SharedViewMember", fields: [userId], references: [id], onDelete: Cascade) + view SharedCalendarView @relation("SharedViewMembers", fields: [viewId], references: [id], onDelete: Cascade) + user User @relation("SharedViewMember", fields: [userId], references: [id], onDelete: Cascade) @@unique([viewId, userId], map: "idx_shared_members_view_user") @@index([userId], map: "idx_shared_members_user") @@ -5297,33 +5313,33 @@ model SharedCalendarMember { } model SharedViewComment { - id String @id @default(cuid()) - viewId String @map("view_id") - userId String @map("user_id") - itemDate String @map("item_date") // YYYY-MM-DD - itemId String? @map("item_id") // specific item reference - content String @db.Text - createdAt DateTime @default(now()) @map("created_at") + id String @id @default(cuid()) + viewId String @map("view_id") + userId String @map("user_id") + itemDate String @map("item_date") // YYYY-MM-DD + itemId String? @map("item_id") // specific item reference + content String @db.Text + createdAt DateTime @default(now()) @map("created_at") // Relations - view SharedCalendarView @relation("SharedViewComments", fields: [viewId], references: [id], onDelete: Cascade) - user User @relation("SharedViewCommentUser", fields: [userId], references: [id], onDelete: Cascade) + view SharedCalendarView @relation("SharedViewComments", fields: [viewId], references: [id], onDelete: Cascade) + user User @relation("SharedViewCommentUser", fields: [userId], references: [id], onDelete: Cascade) @@index([viewId, itemDate], map: "idx_shared_comments_view_date") @@map("shared_view_comments") } model SharedViewReaction { - id String @id @default(cuid()) - viewId String @map("view_id") - userId String @map("user_id") - itemId String @map("item_id") + id String @id @default(cuid()) + viewId String @map("view_id") + userId String @map("user_id") + itemId String @map("item_id") emoji String - createdAt DateTime @default(now()) @map("created_at") + createdAt DateTime @default(now()) @map("created_at") // Relations - view SharedCalendarView @relation("SharedViewReactions", fields: [viewId], references: [id], onDelete: Cascade) - user User @relation("SharedViewReactionUser", fields: [userId], references: [id], onDelete: Cascade) + view SharedCalendarView @relation("SharedViewReactions", fields: [viewId], references: [id], onDelete: Cascade) + user User @relation("SharedViewReactionUser", fields: [userId], references: [id], onDelete: Cascade) @@unique([viewId, userId, itemId, emoji], map: "idx_shared_reactions_unique") @@map("shared_view_reactions") @@ -5338,7 +5354,7 @@ model CalendarExportToken { createdAt DateTime @default(now()) @map("created_at") // Relations - user User @relation("CalendarExportTokenOwner", fields: [userId], references: [id], onDelete: Cascade) + user User @relation("CalendarExportTokenOwner", fields: [userId], references: [id], onDelete: Cascade) @@index([userId], map: "idx_calendar_export_tokens_user") @@map("calendar_export_tokens") @@ -5350,8 +5366,8 @@ model CalendarExportToken { model DocCollabState { id String @id @default(cuid()) - documentId String @unique @map("document_id") // file path, e.g. "admin/index.md" - state Bytes // Y.Doc binary state + documentId String @unique @map("document_id") // file path, e.g. "admin/index.md" + state Bytes // Y.Doc binary state updatedAt DateTime @updatedAt @map("updated_at") createdAt DateTime @default(now()) @map("created_at") @@ -5368,7 +5384,7 @@ enum DocShareLinkStatus { model DocAccessPolicy { id String @id @default(cuid()) - documentPath String @unique @map("document_path") // e.g. "admin/index.md" or "guides/" (trailing slash = directory) + documentPath String @unique @map("document_path") // e.g. "admin/index.md" or "guides/" (trailing slash = directory) isDirectory Boolean @default(false) @map("is_directory") allowedEditors Json @default("[]") @map("allowed_editors") // ["role:CONTENT_ADMIN", "user:clxyz", "all_content_editors"] createdById String @map("created_by_id") @@ -5381,19 +5397,19 @@ model DocAccessPolicy { } model DocShareLink { - id String @id @default(cuid()) - documentPath String @map("document_path") - shareToken String @unique @map("share_token") - status DocShareLinkStatus @default(ACTIVE) - canEdit Boolean @default(true) @map("can_edit") - expiresAt DateTime? @map("expires_at") - maxUses Int? @map("max_uses") - useCount Int @default(0) @map("use_count") - guestName String? @map("guest_name") - createdById String @map("created_by_id") - createdBy User @relation("DocShareLinkCreator", fields: [createdById], references: [id], onDelete: Cascade) - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") + id String @id @default(cuid()) + documentPath String @map("document_path") + shareToken String @unique @map("share_token") + status DocShareLinkStatus @default(ACTIVE) + canEdit Boolean @default(true) @map("can_edit") + expiresAt DateTime? @map("expires_at") + maxUses Int? @map("max_uses") + useCount Int @default(0) @map("use_count") + guestName String? @map("guest_name") + createdById String @map("created_by_id") + createdBy User @relation("DocShareLinkCreator", fields: [createdById], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") @@index([documentPath]) @@index([createdById]) @@ -5417,43 +5433,43 @@ model DocWatch { // ============================================================================ 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) + 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") + 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? @map("other_accessibility") @db.Text // 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") + 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? @map("other_dietary") @db.Text // 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") + needsChildcare Boolean @default(false) @map("needs_childcare") + childcareDetails String? @map("childcare_details") @db.Text + needsTransportation Boolean @default(false) @map("needs_transportation") + transportationNotes String? @map("transportation_notes") @db.Text // Communication - preferredLanguage String? @default("en") @map("preferred_language") - needsTranslation Boolean @default(false) @map("needs_translation") - translationLanguage String? @map("translation_language") + 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") + visibilityConsent String @default("organizer_only") @map("visibility_consent") - updatedAt DateTime @updatedAt @map("updated_at") + updatedAt DateTime @updatedAt @map("updated_at") @@map("participant_needs") } @@ -5463,18 +5479,18 @@ model ParticipantNeeds { // ============================================================================ 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) + 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" + title String + items Json @default("[]") // Array<{ id, title, durationMinutes, presenterUserId, notes, order }> + status String @default("draft") // "draft" | "active" | "completed" - minutes MeetingMinutes? - actionItems ActionItem[] + minutes MeetingMinutes? + actionItems ActionItem[] createdByUserId String @map("created_by_user_id") createdBy User @relation("AgendaCreator", fields: [createdByUserId], references: [id]) @@ -5485,13 +5501,13 @@ model MeetingAgenda { } model MeetingMinutes { - id String @id @default(cuid()) - agendaId String @unique @map("agenda_id") - agenda MeetingAgenda @relation(fields: [agendaId], references: [id], onDelete: Cascade) + 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? }> + 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") @@ -5505,17 +5521,17 @@ model MeetingMinutes { } model ActionItem { - id String @id @default(cuid()) - agendaId String? @map("agenda_id") - agenda MeetingAgenda? @relation(fields: [agendaId], references: [id], onDelete: SetNull) + 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" + 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") @@ -5560,29 +5576,29 @@ enum StrawPollResultVisibility { } model StrawPoll { - id String @id @default(cuid()) - slug String @unique - title String @db.VarChar(200) - description String? @db.Text - type StrawPollType - status StrawPollStatus @default(DRAFT) - identityMode StrawPollIdentityMode @default(ANONYMOUS) @map("identity_mode") - resultVisibility StrawPollResultVisibility @default(LIVE) @map("result_visibility") - allowComments Boolean @default(true) @map("allow_comments") - closesAt DateTime? @map("closes_at") - closeThreshold Int? @map("close_threshold") - autoCloseJobId String? @map("auto_close_job_id") - isPrivate Boolean @default(false) @map("is_private") + id String @id @default(cuid()) + slug String @unique + title String @db.VarChar(200) + description String? @db.Text + type StrawPollType + status StrawPollStatus @default(DRAFT) + identityMode StrawPollIdentityMode @default(ANONYMOUS) @map("identity_mode") + resultVisibility StrawPollResultVisibility @default(LIVE) @map("result_visibility") + allowComments Boolean @default(true) @map("allow_comments") + closesAt DateTime? @map("closes_at") + closeThreshold Int? @map("close_threshold") + autoCloseJobId String? @map("auto_close_job_id") + isPrivate Boolean @default(false) @map("is_private") - createdByUserId String @map("created_by_user_id") - createdBy User @relation("StrawPollCreator", fields: [createdByUserId], references: [id]) - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") + createdByUserId String @map("created_by_user_id") + createdBy User @relation("StrawPollCreator", fields: [createdByUserId], references: [id]) + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") - options StrawPollOption[] - votes StrawPollVote[] - comments StrawPollComment[] - challenges StrawPollChallenge[] + options StrawPollOption[] + votes StrawPollVote[] + comments StrawPollComment[] + challenges StrawPollChallenge[] @@index([createdByUserId]) @@index([status]) @@ -5590,35 +5606,35 @@ model StrawPoll { } model StrawPollOption { - id String @id @default(cuid()) - pollId String @map("poll_id") + id String @id @default(cuid()) + pollId String @map("poll_id") poll StrawPoll @relation(fields: [pollId], references: [id], onDelete: Cascade) - label String @db.VarChar(500) - sortOrder Int @default(0) @map("sort_order") + label String @db.VarChar(500) + sortOrder Int @default(0) @map("sort_order") - votes StrawPollVote[] + votes StrawPollVote[] @@index([pollId]) @@map("straw_poll_options") } model StrawPollVote { - id String @id @default(cuid()) - pollId String @map("poll_id") - poll StrawPoll @relation(fields: [pollId], references: [id], onDelete: Cascade) - optionId String @map("option_id") - option StrawPollOption @relation(fields: [optionId], references: [id], onDelete: Cascade) + id String @id @default(cuid()) + pollId String @map("poll_id") + poll StrawPoll @relation(fields: [pollId], references: [id], onDelete: Cascade) + optionId String @map("option_id") + option StrawPollOption @relation(fields: [optionId], references: [id], onDelete: Cascade) userId String? @map("user_id") user User? @relation("StrawPollVoter", fields: [userId], references: [id], onDelete: SetNull) - voterName String? @db.VarChar(100) @map("voter_name") + voterName String? @map("voter_name") @db.VarChar(100) voterToken String? @map("voter_token") voterIp String? @map("voter_ip") contactId String? @map("contact_id") contact Contact? @relation("StrawPollVoteContact", fields: [contactId], references: [id], onDelete: SetNull) - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") @@unique([pollId, userId]) @@unique([pollId, voterToken]) @@ -5629,14 +5645,14 @@ model StrawPollVote { } model StrawPollComment { - id String @id @default(cuid()) - pollId String @map("poll_id") + id String @id @default(cuid()) + pollId String @map("poll_id") poll StrawPoll @relation(fields: [pollId], references: [id], onDelete: Cascade) - userId String? @map("user_id") - user User? @relation("StrawPollCommenter", fields: [userId], references: [id], onDelete: SetNull) - authorName String @db.VarChar(100) @map("author_name") - content String @db.Text - createdAt DateTime @default(now()) @map("created_at") + userId String? @map("user_id") + user User? @relation("StrawPollCommenter", fields: [userId], references: [id], onDelete: SetNull) + authorName String @map("author_name") @db.VarChar(100) + content String @db.Text + createdAt DateTime @default(now()) @map("created_at") @@index([pollId]) @@map("straw_poll_comments") @@ -5656,3 +5672,123 @@ model StrawPollChallenge { @@unique([pollId, challengerUserId, challengedUserId]) @@map("straw_poll_challenges") } + +// ============================================================================ +// VOLUNTEER DASHBOARD — ACTION CAMPAIGNS +// ============================================================================ +// Stacked-action mini-campaigns: an admin defines an ordered list of steps +// (watch a video, sign a petition, RSVP an event, etc.) that volunteers +// complete to earn recognition or a draw entry. Completion is detected by +// querying the per-user model for each step's target entity. + +enum ActionStepKind { + WATCH_VIDEO + SUBMIT_INFLUENCE + SIGN_PETITION + RSVP_EVENT + SIGNUP_SHIFT + JOIN_CHALLENGE + VISIT_LINK + CUSTOM +} + +enum ActionStepCompletionSource { + AUTO + SELF_REPORTED +} + +model ActionCampaign { + id String @id @default(cuid()) + slug String @unique + title String + description String? @db.Text + rewardText String? @map("reward_text") + isActive Boolean @default(false) @map("is_active") + startsAt DateTime? @map("starts_at") + endsAt DateTime? @map("ends_at") + minStepsForReward Int? @map("min_steps_for_reward") // null = all steps required + createdByUserId String? @map("created_by_user_id") + createdBy User? @relation("ActionCampaignCreator", fields: [createdByUserId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + steps ActionStep[] + + @@index([isActive], map: "idx_action_campaigns_active") + @@map("action_campaigns") +} + +model ActionStep { + id String @id @default(cuid()) + campaignId String @map("campaign_id") + campaign ActionCampaign @relation(fields: [campaignId], references: [id], onDelete: Cascade) + order Int + kind ActionStepKind + label String + description String? @db.Text + targetId String? @map("target_id") // entity id/slug for the kind + targetUrl String? @map("target_url") // optional deep-link override for the user + autoComplete Boolean @default(true) @map("auto_complete") + createdAt DateTime @default(now()) @map("created_at") + + completions ActionStepCompletion[] + + @@unique([campaignId, order]) + @@index([campaignId], map: "idx_action_steps_campaign") + @@map("action_steps") +} + +model ActionStepCompletion { + id String @id @default(cuid()) + userId String @map("user_id") + user User @relation("ActionStepCompleter", fields: [userId], references: [id], onDelete: Cascade) + stepId String @map("step_id") + step ActionStep @relation(fields: [stepId], references: [id], onDelete: Cascade) + source ActionStepCompletionSource @default(AUTO) + completedAt DateTime @default(now()) @map("completed_at") + + @@unique([userId, stepId]) + @@index([userId], map: "idx_action_step_completions_user") + @@map("action_step_completions") +} + +// ============================================================================ +// MEDIA — DOCUMENTS (PDFs and other downloadable assets) +// ============================================================================ +// Image-specific Photo model can't host PDFs (EXIF/sharp pipeline assumes +// raster images), so documents are a distinct first-class media type with +// their own download endpoint and tag-based categorization. + +model Document { + id String @id @default(cuid()) + path String @unique // Full path to file on disk + filename String // UUID filename + originalFilename String? @map("original_filename") + title String? + description String? @db.Text + mimeType String @map("mime_type") + fileSize BigInt? @map("file_size") + pageCount Int? @map("page_count") // For PDFs + thumbnailPath String? @map("thumbnail_path") + + // Categorization + category String? + tags Json? // String array — used for "volunteer-resource" tagging + + // Publishing + isPublished Boolean @default(true) @map("is_published") + position Int? @default(0) + + // Tracking + uploaderId String? @map("uploader_id") + uploader User? @relation("DocumentUploader", fields: [uploaderId], references: [id], onDelete: SetNull) + downloadCount Int @default(0) @map("download_count") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@index([isPublished], map: "idx_documents_published") + @@index([category], map: "idx_documents_category") + @@index([createdAt], map: "idx_documents_created_at") + @@map("documents") +}