Okay Wish I could say I know exactly. Will do better next time promise lol
This commit is contained in:
22
api/prisma/init-gitea-db.sh
Executable file
22
api/prisma/init-gitea-db.sh
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
###############################################################################
|
||||
# Gitea Database Initialization Script
|
||||
###############################################################################
|
||||
# Creates a separate PostgreSQL database for Gitea git hosting.
|
||||
#
|
||||
# Database: gitea
|
||||
# Purpose: Stores Gitea repositories, users, and configuration
|
||||
# Runs: Automatically on first PostgreSQL container startup via docker-entrypoint-initdb.d
|
||||
###############################################################################
|
||||
set -e
|
||||
|
||||
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
|
||||
-- Create Gitea database if it doesn't exist
|
||||
SELECT 'CREATE DATABASE gitea'
|
||||
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'gitea')\gexec
|
||||
|
||||
-- Grant all privileges to the main user
|
||||
GRANT ALL PRIVILEGES ON DATABASE gitea TO ${POSTGRES_USER};
|
||||
EOSQL
|
||||
|
||||
echo "Gitea database 'gitea' created successfully"
|
||||
3823
api/prisma/migrations/20260224100000_baseline_catchup/migration.sql
Normal file
3823
api/prisma/migrations/20260224100000_baseline_catchup/migration.sql
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "ads" ADD COLUMN "placements" JSONB DEFAULT '[]';
|
||||
@@ -0,0 +1,14 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "site_settings" ADD COLUMN "notifyVolunteerShiftThankYou" BOOLEAN NOT NULL DEFAULT true,
|
||||
ADD COLUMN "notify_volunteer_reengagement" BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN "reengagement_cooldown_days" INTEGER NOT NULL DEFAULT 30,
|
||||
ADD COLUMN "reengagement_inactive_days" INTEGER NOT NULL DEFAULT 30;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "sms_conversations" ADD COLUMN "contactId" TEXT;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "sms_conversations_contactId_idx" ON "sms_conversations"("contactId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "sms_conversations" ADD CONSTRAINT "sms_conversations_contactId_fkey" FOREIGN KEY ("contactId") REFERENCES "contacts"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "landing_pages" ADD COLUMN "listed" BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "site_settings" ADD COLUMN "homepage_tagline" TEXT;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "site_settings" ADD COLUMN "enable_social" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "notification_preferences" ADD COLUMN "digest_frequency" TEXT NOT NULL DEFAULT 'none',
|
||||
ADD COLUMN "last_digest_sent_at" TIMESTAMP(3);
|
||||
@@ -0,0 +1,38 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "SocialGroupType" AS ENUM ('SHIFT_TEAM', 'CAMPAIGN_TEAM', 'CUSTOM');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "social_groups" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"type" "SocialGroupType" NOT NULL,
|
||||
"reference_id" TEXT,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "social_groups_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "social_group_members" (
|
||||
"id" TEXT NOT NULL,
|
||||
"group_id" TEXT NOT NULL,
|
||||
"user_id" TEXT NOT NULL,
|
||||
"joined_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "social_group_members_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "idx_social_groups_type_ref" ON "social_groups"("type", "reference_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "idx_social_group_members_user" ON "social_group_members"("user_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "idx_social_group_members_unique" ON "social_group_members"("group_id", "user_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "social_group_members" ADD CONSTRAINT "social_group_members_group_id_fkey" FOREIGN KEY ("group_id") REFERENCES "social_groups"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "social_group_members" ADD CONSTRAINT "social_group_members_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "site_settings" ADD COLUMN "enable_meet" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -0,0 +1,25 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "meetings" (
|
||||
"id" TEXT NOT NULL,
|
||||
"slug" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"jitsi_room" TEXT NOT NULL,
|
||||
"is_active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"created_by_user_id" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
"start_time" TIMESTAMP(3),
|
||||
"end_time" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "meetings_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "meetings_slug_key" ON "meetings"("slug");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "meetings_jitsi_room_key" ON "meetings"("jitsi_room");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "meetings" ADD CONSTRAINT "meetings_created_by_user_id_fkey" FOREIGN KEY ("created_by_user_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,20 @@
|
||||
-- AlterEnum
|
||||
ALTER TYPE "NotificationType" ADD VALUE 'group_call';
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "shifts" ADD COLUMN "meetingId" TEXT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "social_groups" ADD COLUMN "meeting_id" TEXT;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "shifts_meetingId_key" ON "shifts"("meetingId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "social_groups_meeting_id_key" ON "social_groups"("meeting_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "shifts" ADD CONSTRAINT "shifts_meetingId_fkey" FOREIGN KEY ("meetingId") REFERENCES "meetings"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "social_groups" ADD CONSTRAINT "social_groups_meeting_id_fkey" FOREIGN KEY ("meeting_id") REFERENCES "meetings"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -116,6 +116,7 @@ model User {
|
||||
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")
|
||||
@@ -140,6 +141,20 @@ model User {
|
||||
albumsCreated PhotoAlbum[] @relation("AlbumCreator")
|
||||
photoComments PhotoComment[] @relation("PhotoCommentUser")
|
||||
|
||||
// SMS campaign relations
|
||||
smsContactListsCreated SmsContactList[] @relation("SmsContactListCreator")
|
||||
smsCampaignsCreated SmsCampaign[] @relation("SmsCampaignCreator")
|
||||
smsTemplatesCreated SmsMessageTemplate[] @relation("SmsTemplateCreator")
|
||||
|
||||
// Donation pages
|
||||
donationPagesCreated DonationPage[] @relation("DonationPageCreator")
|
||||
|
||||
// Meetings (Jitsi)
|
||||
meetingsCreated Meeting[] @relation("MeetingCreator")
|
||||
|
||||
// People CRM
|
||||
contact Contact? @relation("UserContact")
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
@@ -228,6 +243,7 @@ model Campaign {
|
||||
responses RepresentativeResponse[]
|
||||
customRecipients CustomRecipient[]
|
||||
calls Call[]
|
||||
smsCampaigns SmsCampaign[] @relation("SmsCampaigns")
|
||||
|
||||
@@index([moderationStatus])
|
||||
@@index([isUserGenerated])
|
||||
@@ -578,6 +594,7 @@ model Address {
|
||||
|
||||
// Relations
|
||||
canvassVisits CanvassVisit[]
|
||||
contactAddresses ContactAddress[]
|
||||
|
||||
@@index([locationId])
|
||||
@@index([locationId, id])
|
||||
@@ -657,6 +674,10 @@ model Shift {
|
||||
// Gancio event sync
|
||||
gancioEventId Int?
|
||||
|
||||
// Video briefing meeting
|
||||
meetingId String? @unique
|
||||
meeting Meeting? @relation("ShiftMeeting", fields: [meetingId], references: [id], onDelete: SetNull)
|
||||
|
||||
createdBy String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
@@ -793,7 +814,13 @@ model MapSettings {
|
||||
qrCode2Label String?
|
||||
qrCode3Url String?
|
||||
qrCode3Label String?
|
||||
publicMapEnabled Boolean @default(true)
|
||||
publicMapEnabled Boolean @default(true)
|
||||
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
|
||||
@@ -827,6 +854,7 @@ model SiteSettings {
|
||||
// Text
|
||||
footerText String @default("Powered by Changemaker Lite")
|
||||
loginSubtitle String @default("Admin")
|
||||
homepageTagline String? @map("homepage_tagline")
|
||||
|
||||
// Email branding
|
||||
emailFromName String @default("Changemaker Lite")
|
||||
@@ -856,6 +884,27 @@ model SiteSettings {
|
||||
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")
|
||||
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")
|
||||
|
||||
// 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
|
||||
|
||||
// Notification settings
|
||||
notifyAdminShiftSignup Boolean @default(true)
|
||||
@@ -865,6 +914,24 @@ model SiteSettings {
|
||||
notifyVolunteerSessionSummary Boolean @default(true)
|
||||
notifyVolunteerCancellation Boolean @default(true)
|
||||
notifyVolunteerShiftReminder Boolean @default(true)
|
||||
notifyVolunteerShiftThankYou Boolean @default(true)
|
||||
|
||||
// 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")
|
||||
|
||||
// Navigation configuration (JSON: { items: NavItem[] })
|
||||
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'
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
@@ -1006,6 +1073,7 @@ model LandingPage {
|
||||
mkdocsHideToc Boolean @default(true)
|
||||
mkdocsSkipExport Boolean @default(false)
|
||||
published Boolean @default(false)
|
||||
listed Boolean @default(false)
|
||||
seoTitle String?
|
||||
seoDescription String? @db.Text
|
||||
seoImage String?
|
||||
@@ -1381,6 +1449,7 @@ enum NotificationType {
|
||||
upload_rejected
|
||||
achievement
|
||||
system
|
||||
group_call
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -2086,6 +2155,7 @@ model Ad {
|
||||
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")
|
||||
@@ -2296,6 +2366,43 @@ model CloseFriend {
|
||||
@@map("close_friends")
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SOCIAL GROUPS
|
||||
// ============================================================================
|
||||
|
||||
enum SocialGroupType {
|
||||
SHIFT_TEAM
|
||||
CAMPAIGN_TEAM
|
||||
CUSTOM
|
||||
}
|
||||
|
||||
model SocialGroup {
|
||||
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")
|
||||
members SocialGroupMember[]
|
||||
|
||||
@@unique([type, referenceId], map: "idx_social_groups_type_ref")
|
||||
@@map("social_groups")
|
||||
}
|
||||
|
||||
model SocialGroupMember {
|
||||
id String @id @default(cuid())
|
||||
groupId String @map("group_id")
|
||||
group SocialGroup @relation(fields: [groupId], references: [id], onDelete: Cascade)
|
||||
userId String @map("user_id")
|
||||
user User @relation("SocialGroupMember", fields: [userId], references: [id], onDelete: Cascade)
|
||||
joinedAt DateTime @default(now()) @map("joined_at")
|
||||
|
||||
@@unique([groupId, userId], map: "idx_social_group_members_unique")
|
||||
@@index([userId], map: "idx_social_group_members_user")
|
||||
@@map("social_group_members")
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// USER UPLOADS
|
||||
// ============================================================================
|
||||
@@ -3121,6 +3228,15 @@ model SubscriptionPlan {
|
||||
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")
|
||||
|
||||
// Relations
|
||||
subscriptions UserSubscription[]
|
||||
|
||||
@@ -3237,6 +3353,9 @@ model Product {
|
||||
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")
|
||||
@@ -3276,16 +3395,61 @@ model Order {
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
// Relations
|
||||
user User? @relation("UserOrders", fields: [userId], references: [id])
|
||||
product Product? @relation(fields: [productId], references: [id])
|
||||
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)
|
||||
|
||||
@@index([userId], map: "idx_orders_user")
|
||||
@@index([productId], map: "idx_orders_product")
|
||||
@@index([status], map: "idx_orders_status")
|
||||
@@index([type], map: "idx_orders_type")
|
||||
@@index([donationPageId], map: "idx_orders_donation_page")
|
||||
@@map("orders")
|
||||
}
|
||||
|
||||
enum DonationPageStatus {
|
||||
DRAFT
|
||||
ACTIVE
|
||||
PAUSED
|
||||
ARCHIVED
|
||||
}
|
||||
|
||||
model DonationPage {
|
||||
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")
|
||||
|
||||
// Media
|
||||
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")
|
||||
|
||||
// Creator tracking
|
||||
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")
|
||||
|
||||
orders Order[] @relation("DonationPageOrders")
|
||||
|
||||
@@index([status])
|
||||
@@map("donation_pages")
|
||||
}
|
||||
|
||||
model PaymentSettings {
|
||||
id String @id @default(cuid())
|
||||
stripeSecretKey String @default("") @map("stripe_secret_key")
|
||||
@@ -3333,19 +3497,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")
|
||||
createdAt DateTime @default(now()) @map("created_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")
|
||||
@@ -3670,3 +3836,455 @@ model PhotoReaction {
|
||||
@@index([photoId], map: "idx_photo_reactions_photo")
|
||||
@@map("photo_reactions")
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DOCS COMMENTS (Gitea Issues-backed)
|
||||
// ============================================================================
|
||||
|
||||
enum DocsCommentStatus {
|
||||
PENDING
|
||||
APPROVED
|
||||
REJECTED
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@index([pagePath, status])
|
||||
@@index([status, createdAt])
|
||||
@@index([giteaCommentId])
|
||||
@@map("docs_comments")
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SMS CAMPAIGNS
|
||||
// ============================================================================
|
||||
|
||||
enum SmsContactListStatus {
|
||||
ACTIVE
|
||||
ARCHIVED
|
||||
}
|
||||
|
||||
enum SmsCampaignStatus {
|
||||
DRAFT
|
||||
RUNNING
|
||||
PAUSED
|
||||
COMPLETED
|
||||
FAILED
|
||||
}
|
||||
|
||||
enum SmsMessageDirection {
|
||||
OUTBOUND
|
||||
INBOUND
|
||||
}
|
||||
|
||||
enum SmsMessageStatus {
|
||||
PENDING
|
||||
SENT
|
||||
FAILED
|
||||
DELIVERED
|
||||
}
|
||||
|
||||
enum SmsResponseType {
|
||||
POSITIVE
|
||||
NEGATIVE
|
||||
QUESTION
|
||||
OPT_OUT
|
||||
NEUTRAL
|
||||
}
|
||||
|
||||
enum SmsConversationStatus {
|
||||
ACTIVE
|
||||
OPTED_OUT
|
||||
CLOSED
|
||||
}
|
||||
|
||||
model SmsContactList {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
originalFilename String?
|
||||
totalContacts Int @default(0)
|
||||
status SmsContactListStatus @default(ACTIVE)
|
||||
createdByUserId String?
|
||||
createdByUser User? @relation("SmsContactListCreator", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
entries SmsContactListEntry[]
|
||||
campaigns SmsCampaign[]
|
||||
|
||||
@@map("sms_contact_lists")
|
||||
}
|
||||
|
||||
model SmsContactListEntry {
|
||||
id String @id @default(cuid())
|
||||
listId String
|
||||
list SmsContactList @relation(fields: [listId], references: [id], onDelete: Cascade)
|
||||
phone String
|
||||
name String?
|
||||
email String?
|
||||
customFields Json? // Arbitrary key-value pairs from CSV columns
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@unique([listId, phone])
|
||||
@@index([listId])
|
||||
@@index([phone])
|
||||
@@map("sms_contact_list_entries")
|
||||
}
|
||||
|
||||
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?
|
||||
|
||||
// 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
|
||||
|
||||
recipients SmsCampaignRecipient[]
|
||||
messages SmsMessage[]
|
||||
conversations SmsConversation[]
|
||||
|
||||
@@index([status])
|
||||
@@index([contactListId])
|
||||
@@index([advocacyCampaignId])
|
||||
@@map("sms_campaigns")
|
||||
}
|
||||
|
||||
model SmsCampaignRecipient {
|
||||
id String @id @default(cuid())
|
||||
campaignId String
|
||||
campaign SmsCampaign @relation(fields: [campaignId], references: [id], onDelete: Cascade)
|
||||
phone String
|
||||
name String?
|
||||
status SmsMessageStatus @default(PENDING)
|
||||
sentAt DateTime?
|
||||
errorMessage String?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([campaignId, status])
|
||||
@@index([phone])
|
||||
@@map("sms_campaign_recipients")
|
||||
}
|
||||
|
||||
model SmsMessage {
|
||||
id String @id @default(cuid())
|
||||
phone String
|
||||
message String @db.Text
|
||||
direction SmsMessageDirection
|
||||
status SmsMessageStatus @default(PENDING)
|
||||
connectionType String? // e.g. "termux"
|
||||
|
||||
// Campaign context (nullable for ad-hoc messages)
|
||||
campaignId String?
|
||||
campaign SmsCampaign? @relation(fields: [campaignId], references: [id], onDelete: SetNull)
|
||||
conversationId String?
|
||||
conversation SmsConversation? @relation(fields: [conversationId], references: [id], onDelete: SetNull)
|
||||
|
||||
// Response classification (for inbound messages)
|
||||
responseType SmsResponseType?
|
||||
isRead Boolean @default(false)
|
||||
sentAt DateTime @default(now())
|
||||
|
||||
@@index([phone])
|
||||
@@index([campaignId])
|
||||
@@index([conversationId])
|
||||
@@index([direction, sentAt])
|
||||
@@map("sms_messages")
|
||||
}
|
||||
|
||||
model SmsConversation {
|
||||
id String @id @default(cuid())
|
||||
phone String
|
||||
contactName String?
|
||||
campaignId String?
|
||||
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)
|
||||
lastMessageAt DateTime?
|
||||
lastResponseAt DateTime?
|
||||
notes String? @db.Text
|
||||
tags Json? // String array
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
messages SmsMessage[]
|
||||
|
||||
@@unique([phone, campaignId])
|
||||
@@index([status])
|
||||
@@index([lastMessageAt])
|
||||
@@index([contactId])
|
||||
@@map("sms_conversations")
|
||||
}
|
||||
|
||||
model SmsMessageTemplate {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
template String @db.Text
|
||||
description String?
|
||||
category String?
|
||||
isFavorite Boolean @default(false)
|
||||
usageCount Int @default(0)
|
||||
createdByUserId String?
|
||||
createdByUser User? @relation("SmsTemplateCreator", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("sms_message_templates")
|
||||
}
|
||||
|
||||
model SmsDeviceStatus {
|
||||
id String @id @default(cuid())
|
||||
isConnected Boolean @default(false)
|
||||
connectionType String?
|
||||
batteryLevel Int?
|
||||
batteryStatus String?
|
||||
totalSent Int @default(0)
|
||||
lastCheckedAt DateTime @default(now())
|
||||
|
||||
@@map("sms_device_status")
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PEOPLE CRM
|
||||
// ============================================================================
|
||||
|
||||
enum ContactSource {
|
||||
USER
|
||||
ADDRESS_OCCUPANT
|
||||
CAMPAIGN_SENDER
|
||||
SHIFT_SIGNUP
|
||||
SMS_CONTACT
|
||||
DONATION
|
||||
MANUAL
|
||||
}
|
||||
|
||||
enum ConnectionType {
|
||||
HOUSEHOLD
|
||||
FAMILY
|
||||
COLLEAGUE
|
||||
REFERRED_BY
|
||||
CUSTOM
|
||||
}
|
||||
|
||||
enum ContactActivityType {
|
||||
EMAIL_SENT
|
||||
RESPONSE_SUBMITTED
|
||||
SHIFT_SIGNUP
|
||||
CANVASS_VISIT
|
||||
DONATION
|
||||
PURCHASE
|
||||
SMS_SENT
|
||||
SMS_RECEIVED
|
||||
VIDEO_VIEW
|
||||
NOTE_ADDED
|
||||
CONTACT_MERGED
|
||||
PROFILE_SELF_EDIT
|
||||
PROFILE_PHOTO_UPDATED
|
||||
USER_LOGIN
|
||||
}
|
||||
|
||||
model Contact {
|
||||
id String @id @default(cuid())
|
||||
displayName String
|
||||
firstName String?
|
||||
lastName String?
|
||||
email String?
|
||||
phone String?
|
||||
|
||||
// CRM data
|
||||
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)
|
||||
|
||||
// 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
|
||||
|
||||
// Source tracking
|
||||
primarySource ContactSource @default(MANUAL)
|
||||
|
||||
// Links to existing models
|
||||
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")
|
||||
|
||||
// Audit
|
||||
createdByUserId String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// Relations
|
||||
addresses ContactAddress[]
|
||||
emails ContactEmail[]
|
||||
phones ContactPhone[]
|
||||
connectionsFrom ContactConnection[] @relation("ConnectionFrom")
|
||||
connectionsTo ContactConnection[] @relation("ConnectionTo")
|
||||
activities ContactActivity[]
|
||||
smsConversations SmsConversation[] @relation("ContactSmsConversations")
|
||||
|
||||
@@index([email])
|
||||
@@index([phone])
|
||||
@@index([displayName])
|
||||
@@index([primarySource])
|
||||
@@index([mergedIntoId])
|
||||
@@map("contacts")
|
||||
}
|
||||
|
||||
model CrmTag {
|
||||
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
|
||||
createdByUserId String?
|
||||
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())
|
||||
|
||||
@@unique([contactId, addressId])
|
||||
@@index([addressId])
|
||||
@@map("contact_addresses")
|
||||
}
|
||||
|
||||
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())
|
||||
|
||||
@@unique([contactId, email])
|
||||
@@index([email])
|
||||
@@index([contactId])
|
||||
@@map("contact_emails")
|
||||
}
|
||||
|
||||
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())
|
||||
|
||||
@@unique([contactId, phone])
|
||||
@@index([phone])
|
||||
@@index([contactId])
|
||||
@@map("contact_phones")
|
||||
}
|
||||
|
||||
model ContactConnection {
|
||||
id String @id @default(cuid())
|
||||
fromContactId String
|
||||
fromContact Contact @relation("ConnectionFrom", fields: [fromContactId], references: [id], onDelete: Cascade)
|
||||
toContactId String
|
||||
toContact Contact @relation("ConnectionTo", fields: [toContactId], references: [id], onDelete: Cascade)
|
||||
type ConnectionType
|
||||
label String?
|
||||
notes String? @db.Text
|
||||
isBidirectional Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@unique([fromContactId, toContactId, type])
|
||||
@@index([toContactId])
|
||||
@@map("contact_connections")
|
||||
}
|
||||
|
||||
model ContactActivity {
|
||||
id String @id @default(cuid())
|
||||
contactId String
|
||||
contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade)
|
||||
type ContactActivityType
|
||||
title String
|
||||
description String? @db.Text
|
||||
metadata Json?
|
||||
occurredAt DateTime @default(now())
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([contactId, occurredAt(sort: Desc)])
|
||||
@@map("contact_activities")
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MEETINGS (JITSI)
|
||||
// ============================================================================
|
||||
|
||||
model Meeting {
|
||||
id String @id @default(cuid())
|
||||
slug String @unique
|
||||
title String
|
||||
description String?
|
||||
jitsiRoom String @unique @map("jitsi_room")
|
||||
isActive Boolean @default(true) @map("is_active")
|
||||
createdByUserId String @map("created_by_user_id")
|
||||
createdBy User @relation("MeetingCreator", fields: [createdByUserId], references: [id])
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
startTime DateTime? @map("start_time")
|
||||
endTime DateTime? @map("end_time")
|
||||
|
||||
// Reverse relations (one-to-one)
|
||||
shift Shift? @relation("ShiftMeeting")
|
||||
group SocialGroup? @relation("GroupMeeting")
|
||||
|
||||
@@map("meetings")
|
||||
}
|
||||
|
||||
@@ -98,11 +98,50 @@ async function main() {
|
||||
smtpActiveProvider: isMailhog ? 'mailhog' : 'production',
|
||||
emailTestMode: env.EMAIL_TEST_MODE === 'true',
|
||||
testEmailRecipient: env.TEST_EMAIL_RECIPIENT,
|
||||
navConfig: {
|
||||
items: [
|
||||
{ id: 'home', label: 'Home', path: '/', icon: 'HomeOutlined', enabled: true, order: 0, type: 'builtin', external: true },
|
||||
{ id: 'campaigns', label: 'Campaigns', path: '/campaigns', icon: 'SendOutlined', enabled: true, order: 1, type: 'builtin', featureFlag: 'enableInfluence' },
|
||||
{ id: 'map', label: 'Map', path: '/map', icon: 'EnvironmentOutlined', enabled: true, order: 2, type: 'builtin', featureFlag: 'enableMap' },
|
||||
{ id: 'shifts', label: 'Shifts', path: '/shifts', icon: 'CalendarOutlined', enabled: true, order: 3, type: 'builtin', featureFlag: 'enableMap' },
|
||||
{ id: 'events', label: 'Events', path: '/events', icon: 'CalendarOutlined', enabled: true, order: 4, type: 'builtin', featureFlag: 'enableEvents', external: true },
|
||||
{ id: 'gallery', label: 'Gallery', path: '/gallery', icon: 'PlayCircleOutlined', enabled: true, order: 5, type: 'builtin', featureFlag: 'enableMediaFeatures' },
|
||||
{ id: 'pricing', label: 'Pricing', path: '/pricing', icon: 'DollarOutlined', enabled: true, order: 6, type: 'builtin', featureFlag: 'enablePayments' },
|
||||
{ id: 'shop', label: 'Shop', path: '/shop', icon: 'ShoppingOutlined', enabled: true, order: 7, type: 'builtin', featureFlag: 'enablePayments' },
|
||||
{ id: 'donate', label: 'Donate', path: '/donate', icon: 'HeartOutlined', enabled: true, order: 8, type: 'builtin', featureFlag: 'enablePayments' },
|
||||
{ id: 'landing', label: 'Website', path: '$landing', icon: 'GlobalOutlined', enabled: false, order: 9, type: 'builtin', external: true },
|
||||
{ id: 'docs', label: 'Docs', path: '$docs', icon: 'BookOutlined', enabled: false, order: 10, type: 'builtin', external: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
console.log('Created SiteSettings with SMTP config from .env');
|
||||
console.log('Created SiteSettings with SMTP config + navConfig from .env');
|
||||
} else {
|
||||
console.log('SiteSettings already exists, skipping SMTP seeding');
|
||||
|
||||
// Seed navConfig if null (existing installations)
|
||||
if (!existingSettings.navConfig) {
|
||||
const defaultNavConfig = {
|
||||
items: [
|
||||
{ id: 'home', label: 'Home', path: '/', icon: 'HomeOutlined', enabled: true, order: 0, type: 'builtin', external: true },
|
||||
{ id: 'campaigns', label: 'Campaigns', path: '/campaigns', icon: 'SendOutlined', enabled: true, order: 1, type: 'builtin', featureFlag: 'enableInfluence' },
|
||||
{ id: 'map', label: 'Map', path: '/map', icon: 'EnvironmentOutlined', enabled: true, order: 2, type: 'builtin', featureFlag: 'enableMap' },
|
||||
{ id: 'shifts', label: 'Shifts', path: '/shifts', icon: 'CalendarOutlined', enabled: true, order: 3, type: 'builtin', featureFlag: 'enableMap' },
|
||||
{ id: 'events', label: 'Events', path: '/events', icon: 'CalendarOutlined', enabled: true, order: 4, type: 'builtin', featureFlag: 'enableEvents', external: true },
|
||||
{ id: 'gallery', label: 'Gallery', path: '/gallery', icon: 'PlayCircleOutlined', enabled: true, order: 5, type: 'builtin', featureFlag: 'enableMediaFeatures' },
|
||||
{ id: 'pricing', label: 'Pricing', path: '/pricing', icon: 'DollarOutlined', enabled: true, order: 6, type: 'builtin', featureFlag: 'enablePayments' },
|
||||
{ id: 'shop', label: 'Shop', path: '/shop', icon: 'ShoppingOutlined', enabled: true, order: 7, type: 'builtin', featureFlag: 'enablePayments' },
|
||||
{ id: 'donate', label: 'Donate', path: '/donate', icon: 'HeartOutlined', enabled: true, order: 8, type: 'builtin', featureFlag: 'enablePayments' },
|
||||
{ id: 'landing', label: 'Website', path: '$landing', icon: 'GlobalOutlined', enabled: false, order: 9, type: 'builtin', external: true },
|
||||
{ id: 'docs', label: 'Docs', path: '$docs', icon: 'BookOutlined', enabled: false, order: 10, type: 'builtin', external: true },
|
||||
],
|
||||
};
|
||||
await prisma.siteSettings.update({
|
||||
where: { id: existingSettings.id },
|
||||
data: { navConfig: defaultNavConfig },
|
||||
});
|
||||
console.log('Seeded default navConfig on existing SiteSettings');
|
||||
}
|
||||
}
|
||||
|
||||
// Create default page blocks for landing page builder
|
||||
@@ -345,6 +384,67 @@ async function main() {
|
||||
title: 'Upcoming Events',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'default-photo',
|
||||
type: 'photo',
|
||||
label: 'Photo',
|
||||
category: 'Media',
|
||||
sortOrder: 14,
|
||||
schema: {
|
||||
photoId: { type: 'number', label: 'Photo ID', required: true },
|
||||
size: { type: 'select', label: 'Size', options: ['thumb', 'medium', 'large'], default: 'large' },
|
||||
caption: { type: 'string', label: 'Caption' },
|
||||
linkToGallery: { type: 'boolean', label: 'Link to Gallery', default: true },
|
||||
alignment: { type: 'select', label: 'Alignment', options: ['left', 'center', 'right'], default: 'center' },
|
||||
maxWidth: { type: 'string', label: 'Max Width', default: '100%' },
|
||||
},
|
||||
defaults: {
|
||||
photoId: null,
|
||||
size: 'large',
|
||||
caption: '',
|
||||
linkToGallery: true,
|
||||
alignment: 'center',
|
||||
maxWidth: '100%',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'default-photo-card',
|
||||
type: 'photo-card',
|
||||
label: 'Photo Card',
|
||||
category: 'Media',
|
||||
sortOrder: 15,
|
||||
schema: {
|
||||
photoId: { type: 'number', label: 'Photo ID', required: true },
|
||||
title: { type: 'string', label: 'Title' },
|
||||
description: { type: 'string', label: 'Description' },
|
||||
showMetadata: { type: 'boolean', label: 'Show Metadata (format, dimensions)', default: true },
|
||||
},
|
||||
defaults: {
|
||||
photoId: null,
|
||||
title: '',
|
||||
description: '',
|
||||
showMetadata: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'default-photo-album',
|
||||
type: 'photo-album',
|
||||
label: 'Photo Album',
|
||||
category: 'Media',
|
||||
sortOrder: 16,
|
||||
schema: {
|
||||
albumId: { type: 'number', label: 'Album ID', required: true },
|
||||
columns: { type: 'select', label: 'Columns', options: ['2', '3', '4'], default: '3' },
|
||||
maxPhotos: { type: 'number', label: 'Max Photos', default: 12 },
|
||||
showTitle: { type: 'boolean', label: 'Show Album Title', default: true },
|
||||
},
|
||||
defaults: {
|
||||
albumId: null,
|
||||
columns: '3',
|
||||
maxPhotos: 12,
|
||||
showTitle: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
for (const block of defaultBlocks) {
|
||||
@@ -648,6 +748,38 @@ async function seedEmailTemplates(admin: { id: string; email: string }) {
|
||||
{ key: 'SIGNUP_URL', label: 'Signup URL', description: 'URL to browse available shifts', isRequired: true, isConditional: false, sampleValue: 'https://app.cmlite.org/shifts', sortOrder: 5 },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'volunteer-shift-thank-you',
|
||||
name: 'Volunteer: Post-Shift Thank You',
|
||||
description: 'Thank-you email sent to a volunteer 2 hours after their shift ends',
|
||||
category: EmailTemplateCategory.MAP,
|
||||
subjectLine: 'Thank you for volunteering — {{SHIFT_TITLE}}',
|
||||
isSystem: true,
|
||||
variables: [
|
||||
{ key: 'ORGANIZATION_NAME', label: 'Organization Name', description: 'Name of the organization', isRequired: true, isConditional: false, sampleValue: 'Changemaker Lite', sortOrder: 0 },
|
||||
{ key: 'VOLUNTEER_NAME', label: 'Volunteer Name', description: 'Name of the volunteer', isRequired: true, isConditional: false, sampleValue: 'Jane Doe', sortOrder: 1 },
|
||||
{ key: 'SHIFT_TITLE', label: 'Shift Title', description: 'Title of the shift', isRequired: true, isConditional: false, sampleValue: 'Weekend Canvassing - Downtown', sortOrder: 2 },
|
||||
{ key: 'SHIFT_DATE', label: 'Shift Date', description: 'Date of the shift', isRequired: true, isConditional: false, sampleValue: 'Saturday, February 22, 2026', sortOrder: 3 },
|
||||
{ key: 'SHIFT_TIME', label: 'Shift Time', description: 'Time range of the shift', isRequired: true, isConditional: false, sampleValue: '10:00 AM — 2:00 PM', sortOrder: 4 },
|
||||
{ key: 'SHIFT_LOCATION', label: 'Shift Location', description: 'Meeting location for the shift', isRequired: false, isConditional: true, sampleValue: 'City Hall', sortOrder: 5 },
|
||||
{ key: 'SIGNUP_URL', label: 'Signup URL', description: 'URL to browse upcoming shifts', isRequired: true, isConditional: false, sampleValue: 'https://app.cmlite.org/shifts', sortOrder: 6 },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'volunteer-reengagement',
|
||||
name: 'Volunteer: Re-Engagement',
|
||||
description: 'Re-engagement email sent to volunteers who have been inactive for a configurable period',
|
||||
category: EmailTemplateCategory.MAP,
|
||||
subjectLine: 'We miss you — {{ORGANIZATION_NAME}}',
|
||||
isSystem: true,
|
||||
variables: [
|
||||
{ key: 'ORGANIZATION_NAME', label: 'Organization Name', description: 'Name of the organization', isRequired: true, isConditional: false, sampleValue: 'Changemaker Lite', sortOrder: 0 },
|
||||
{ key: 'VOLUNTEER_NAME', label: 'Volunteer Name', description: 'Name of the volunteer', isRequired: true, isConditional: false, sampleValue: 'Jane Doe', sortOrder: 1 },
|
||||
{ key: 'LAST_ACTIVITY_DATE', label: 'Last Activity Date', description: 'Date of the volunteer\'s last activity', isRequired: true, isConditional: false, sampleValue: 'January 15, 2026', sortOrder: 2 },
|
||||
{ key: 'LAST_ACTIVITY_TYPE', label: 'Last Activity Type', description: 'Type of the volunteer\'s last activity', isRequired: true, isConditional: false, sampleValue: 'Shift Signup', sortOrder: 3 },
|
||||
{ key: 'SIGNUP_URL', label: 'Signup URL', description: 'URL to browse upcoming shifts', isRequired: true, isConditional: false, sampleValue: 'https://app.cmlite.org/shifts', sortOrder: 4 },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
let seededCount = 0;
|
||||
@@ -741,6 +873,7 @@ async function seedGalleryAds() {
|
||||
iconEmoji: null,
|
||||
bgColor: null,
|
||||
imagePath: null,
|
||||
placements: ['gallery', 'docs'],
|
||||
},
|
||||
{
|
||||
type: 'payment_subscribe',
|
||||
@@ -756,6 +889,7 @@ async function seedGalleryAds() {
|
||||
iconEmoji: null,
|
||||
bgColor: null,
|
||||
imagePath: null,
|
||||
placements: ['gallery', 'pricing', 'landing_page', 'docs'],
|
||||
},
|
||||
{
|
||||
type: 'payment_donate',
|
||||
@@ -771,6 +905,7 @@ async function seedGalleryAds() {
|
||||
iconEmoji: null,
|
||||
bgColor: null,
|
||||
imagePath: null,
|
||||
placements: ['gallery', 'donate', 'landing_page', 'docs'],
|
||||
},
|
||||
{
|
||||
type: 'payment_shop',
|
||||
@@ -786,6 +921,7 @@ async function seedGalleryAds() {
|
||||
iconEmoji: null,
|
||||
bgColor: null,
|
||||
imagePath: null,
|
||||
placements: ['gallery', 'shop', 'landing_page'],
|
||||
},
|
||||
{
|
||||
type: 'system',
|
||||
@@ -801,6 +937,7 @@ async function seedGalleryAds() {
|
||||
iconEmoji: null,
|
||||
bgColor: null,
|
||||
imagePath: null,
|
||||
placements: ['gallery', 'campaigns_list', 'shifts', 'landing_page', 'docs'],
|
||||
},
|
||||
{
|
||||
type: 'system',
|
||||
@@ -816,6 +953,7 @@ async function seedGalleryAds() {
|
||||
iconEmoji: null,
|
||||
bgColor: null,
|
||||
imagePath: null,
|
||||
placements: ['gallery', 'shifts', 'campaigns_list', 'landing_page'],
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user