Add ActionCampaign + Document models for volunteer dashboard
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
This commit is contained in:
parent
5f0ae6bc5a
commit
3fc67cd81a
@ -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;
|
||||||
|
|
||||||
@ -228,6 +228,11 @@ model User {
|
|||||||
petitionsCreated Petition[] @relation("PetitionCreator")
|
petitionsCreated Petition[] @relation("PetitionCreator")
|
||||||
petitionsReviewed Petition[] @relation("PetitionReviewer")
|
petitionsReviewed Petition[] @relation("PetitionReviewer")
|
||||||
|
|
||||||
|
// Volunteer dashboard — action campaigns
|
||||||
|
actionCampaignsCreated ActionCampaign[] @relation("ActionCampaignCreator")
|
||||||
|
actionStepCompletions ActionStepCompletion[] @relation("ActionStepCompleter")
|
||||||
|
documentsUploaded Document[] @relation("DocumentUploader")
|
||||||
|
|
||||||
@@map("users")
|
@@map("users")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -866,6 +871,7 @@ model Shift {
|
|||||||
maxVolunteers Int
|
maxVolunteers Int
|
||||||
currentVolunteers Int @default(0)
|
currentVolunteers Int @default(0)
|
||||||
status ShiftStatus @default(OPEN)
|
status ShiftStatus @default(OPEN)
|
||||||
|
kind ShiftKind @default(CANVASS)
|
||||||
isPublic Boolean @default(false)
|
isPublic Boolean @default(false)
|
||||||
cutId String?
|
cutId String?
|
||||||
cut Cut? @relation(fields: [cutId], references: [id], onDelete: SetNull)
|
cut Cut? @relation(fields: [cutId], references: [id], onDelete: SetNull)
|
||||||
@ -913,6 +919,14 @@ enum SignupSource {
|
|||||||
POLL_CONVERSION
|
POLL_CONVERSION
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ShiftKind {
|
||||||
|
CANVASS
|
||||||
|
TRAINING
|
||||||
|
EVENT_STAFFING
|
||||||
|
PHONE_BANK
|
||||||
|
OTHER
|
||||||
|
}
|
||||||
|
|
||||||
model ShiftSignup {
|
model ShiftSignup {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
shiftId String
|
shiftId String
|
||||||
@ -1453,6 +1467,7 @@ model TrackPoint {
|
|||||||
@@index([recordedAt])
|
@@index([recordedAt])
|
||||||
@@map("track_points")
|
@@map("track_points")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enums
|
// Enums
|
||||||
enum DirectoryType {
|
enum DirectoryType {
|
||||||
studios
|
studios
|
||||||
@ -3488,7 +3503,7 @@ model SubscriptionPlan {
|
|||||||
slug String? @unique
|
slug String? @unique
|
||||||
coverPhoto String? @map("cover_photo")
|
coverPhoto String? @map("cover_photo")
|
||||||
coverVideoId Int? @map("cover_video_id")
|
coverVideoId Int? @map("cover_video_id")
|
||||||
richDescription String? @db.Text @map("rich_description")
|
richDescription String? @map("rich_description") @db.Text
|
||||||
ctaText String? @map("cta_text")
|
ctaText String? @map("cta_text")
|
||||||
ctaSubtext String? @map("cta_subtext")
|
ctaSubtext String? @map("cta_subtext")
|
||||||
highlightPlan Boolean @default(false) @map("highlight_plan")
|
highlightPlan Boolean @default(false) @map("highlight_plan")
|
||||||
@ -3646,7 +3661,7 @@ model Order {
|
|||||||
buyerName String? @map("buyer_name")
|
buyerName String? @map("buyer_name")
|
||||||
|
|
||||||
// Donation-specific
|
// Donation-specific
|
||||||
donorMessage String? @db.Text @map("donor_message")
|
donorMessage String? @map("donor_message") @db.Text
|
||||||
isAnonymous Boolean @default(false) @map("is_anonymous")
|
isAnonymous Boolean @default(false) @map("is_anonymous")
|
||||||
|
|
||||||
completedAt DateTime? @map("completed_at")
|
completedAt DateTime? @map("completed_at")
|
||||||
@ -3689,7 +3704,7 @@ model DonationPage {
|
|||||||
// Donation config (per-page, mirrors PaymentSettings fields)
|
// Donation config (per-page, mirrors PaymentSettings fields)
|
||||||
suggestedAmounts Json @default("[1000, 2500, 5000, 10000]")
|
suggestedAmounts Json @default("[1000, 2500, 5000, 10000]")
|
||||||
minimumAmount Int @default(500) @map("minimum_amount")
|
minimumAmount Int @default(500) @map("minimum_amount")
|
||||||
thankYouMessage String @default("Thank you for your support!") @db.Text @map("thank_you_message")
|
thankYouMessage String @default("Thank you for your support!") @map("thank_you_message") @db.Text
|
||||||
|
|
||||||
// Media
|
// Media
|
||||||
coverPhoto String? @map("cover_photo")
|
coverPhoto String? @map("cover_photo")
|
||||||
@ -3726,8 +3741,8 @@ model PaymentSettings {
|
|||||||
donationSuggestedAmounts Json @default("[1000, 2500, 5000, 10000]") @map("donation_suggested_amounts")
|
donationSuggestedAmounts Json @default("[1000, 2500, 5000, 10000]") @map("donation_suggested_amounts")
|
||||||
donationMinimum Int @default(500) @map("donation_minimum")
|
donationMinimum Int @default(500) @map("donation_minimum")
|
||||||
donationPageTitle String @default("Support Our Work") @map("donation_page_title")
|
donationPageTitle String @default("Support Our Work") @map("donation_page_title")
|
||||||
donationPageDescription String? @db.Text @map("donation_page_description")
|
donationPageDescription String? @map("donation_page_description") @db.Text
|
||||||
thankYouMessage String @default("Thank you for your support!") @db.Text @map("thank_you_message")
|
thankYouMessage String @default("Thank you for your support!") @map("thank_you_message") @db.Text
|
||||||
|
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
updatedAt DateTime @updatedAt @map("updated_at")
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
@ -4951,7 +4966,7 @@ model TicketedEvent {
|
|||||||
slug String @unique
|
slug String @unique
|
||||||
title String
|
title String
|
||||||
description String? @db.Text
|
description String? @db.Text
|
||||||
richDescription String? @db.Text @map("rich_description")
|
richDescription String? @map("rich_description") @db.Text
|
||||||
|
|
||||||
// Schedule
|
// Schedule
|
||||||
date DateTime @db.Date
|
date DateTime @db.Date
|
||||||
@ -4969,6 +4984,7 @@ model TicketedEvent {
|
|||||||
status TicketedEventStatus @default(DRAFT)
|
status TicketedEventStatus @default(DRAFT)
|
||||||
visibility TicketedEventVisibility @default(PUBLIC)
|
visibility TicketedEventVisibility @default(PUBLIC)
|
||||||
inviteCode String? @unique @map("invite_code")
|
inviteCode String? @unique @map("invite_code")
|
||||||
|
featured Boolean @default(false)
|
||||||
|
|
||||||
// Media
|
// Media
|
||||||
coverImageUrl String? @map("cover_image_url")
|
coverImageUrl String? @map("cover_image_url")
|
||||||
@ -5428,7 +5444,7 @@ model ParticipantNeeds {
|
|||||||
needsGroundFloor Boolean @default(false) @map("needs_ground_floor")
|
needsGroundFloor Boolean @default(false) @map("needs_ground_floor")
|
||||||
needsHearingLoop Boolean @default(false) @map("needs_hearing_loop")
|
needsHearingLoop Boolean @default(false) @map("needs_hearing_loop")
|
||||||
needsSignLanguage Boolean @default(false) @map("needs_sign_language")
|
needsSignLanguage Boolean @default(false) @map("needs_sign_language")
|
||||||
otherAccessibility String? @db.Text @map("other_accessibility")
|
otherAccessibility String? @map("other_accessibility") @db.Text
|
||||||
|
|
||||||
// Dietary
|
// Dietary
|
||||||
isVegan Boolean @default(false) @map("is_vegan")
|
isVegan Boolean @default(false) @map("is_vegan")
|
||||||
@ -5437,13 +5453,13 @@ model ParticipantNeeds {
|
|||||||
isHalal Boolean @default(false) @map("is_halal")
|
isHalal Boolean @default(false) @map("is_halal")
|
||||||
isKosher Boolean @default(false) @map("is_kosher")
|
isKosher Boolean @default(false) @map("is_kosher")
|
||||||
hasNutAllergy Boolean @default(false) @map("has_nut_allergy")
|
hasNutAllergy Boolean @default(false) @map("has_nut_allergy")
|
||||||
otherDietary String? @db.Text @map("other_dietary")
|
otherDietary String? @map("other_dietary") @db.Text
|
||||||
|
|
||||||
// Care barriers
|
// Care barriers
|
||||||
needsChildcare Boolean @default(false) @map("needs_childcare")
|
needsChildcare Boolean @default(false) @map("needs_childcare")
|
||||||
childcareDetails String? @db.Text @map("childcare_details")
|
childcareDetails String? @map("childcare_details") @db.Text
|
||||||
needsTransportation Boolean @default(false) @map("needs_transportation")
|
needsTransportation Boolean @default(false) @map("needs_transportation")
|
||||||
transportationNotes String? @db.Text @map("transportation_notes")
|
transportationNotes String? @map("transportation_notes") @db.Text
|
||||||
|
|
||||||
// Communication
|
// Communication
|
||||||
preferredLanguage String? @default("en") @map("preferred_language")
|
preferredLanguage String? @default("en") @map("preferred_language")
|
||||||
@ -5611,7 +5627,7 @@ model StrawPollVote {
|
|||||||
|
|
||||||
userId String? @map("user_id")
|
userId String? @map("user_id")
|
||||||
user User? @relation("StrawPollVoter", fields: [userId], references: [id], onDelete: SetNull)
|
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")
|
voterToken String? @map("voter_token")
|
||||||
voterIp String? @map("voter_ip")
|
voterIp String? @map("voter_ip")
|
||||||
contactId String? @map("contact_id")
|
contactId String? @map("contact_id")
|
||||||
@ -5634,7 +5650,7 @@ model StrawPollComment {
|
|||||||
poll StrawPoll @relation(fields: [pollId], references: [id], onDelete: Cascade)
|
poll StrawPoll @relation(fields: [pollId], references: [id], onDelete: Cascade)
|
||||||
userId String? @map("user_id")
|
userId String? @map("user_id")
|
||||||
user User? @relation("StrawPollCommenter", fields: [userId], references: [id], onDelete: SetNull)
|
user User? @relation("StrawPollCommenter", fields: [userId], references: [id], onDelete: SetNull)
|
||||||
authorName String @db.VarChar(100) @map("author_name")
|
authorName String @map("author_name") @db.VarChar(100)
|
||||||
content String @db.Text
|
content String @db.Text
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
|
||||||
@ -5656,3 +5672,123 @@ model StrawPollChallenge {
|
|||||||
@@unique([pollId, challengerUserId, challengedUserId])
|
@@unique([pollId, challengerUserId, challengedUserId])
|
||||||
@@map("straw_poll_challenges")
|
@@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")
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user