diff --git a/docs/superpowers/plans/2026-06-04-wave3-logistics-email-templates-tab.md b/docs/superpowers/plans/2026-06-04-wave3-logistics-email-templates-tab.md new file mode 100644 index 0000000..00da296 --- /dev/null +++ b/docs/superpowers/plans/2026-06-04-wave3-logistics-email-templates-tab.md @@ -0,0 +1,113 @@ +# Wave 3 — Enable the "Email Templates" tab (logistics hub) + +> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:subagent-driven-development. Steps use `- [ ]`. + +**Goal:** Turn the disabled "Email Templates (soon)" tab in `/admin/logistics` into a working surface where admins can, for every logistics email, toggle it on/off, customize the subject, **preview** the rendered email, and send a test to themselves — reusing the existing notification-settings infra. + +**Architecture:** Reuse `notification.getEmailSettings` / `updateEmailSetting` / `sendTestEmail` (already built) scoped to the `logistics` category (the 8 types seeded in Wave 2). The only new server capability is **preview without sending**: extract a `renderNotificationEmail(...)` from `sendStyledNotificationEmail` and expose a `notification.previewEmailTemplate({ notificationType })` query returning `{ subject, html }`. UI reuses the existing `EmailPreviewDialog` (with `previewOnly`). + +**Tech Stack:** Next.js 15, tRPC 11, shadcn/ui. No schema change. + +**Key facts (verified 2026-06-04):** +- `notification.getEmailSettings` (`src/server/routers/notification.ts:140`) returns all `NotificationEmailSetting` rows (incl. our 8 `category:'logistics'` rows). +- `notification.updateEmailSetting` (`:152`) accepts `{ notificationType, sendEmail, emailSubject?, emailTemplate? }`. +- `notification.sendTestEmail` (`:243`) renders via `sendStyledNotificationEmail` using a per-type `sampleData` map (which currently has NO logistics entries → previews/tests render with template fallbacks). +- `sendStyledNotificationEmail` (`src/lib/email.ts:2400`) looks up `NOTIFICATION_EMAIL_TEMPLATES[type]`, else falls back to `getNotificationEmailTemplate`. Our 4 custom templates are registered; the 4 admin-alert types use the fallback. +- `EmailPreviewDialog` (`src/components/admin/round/email-preview-dialog.tsx`) props: `{ open, onOpenChange, title, description, recipientCount, previewHtml, isPreviewLoading, onSend, isSending, showCustomMessage?, onRefreshPreview?, previewOnly? }`. +- The existing global form `src/components/settings/notification-settings-form.tsx` renders categories team/jury/mentor/observer/admin — NOT `logistics`. + +--- + +## Task 1: Server — render-without-send + preview query + logistics sample data + +**Files:** `src/lib/email.ts`, `src/server/routers/notification.ts`; test `tests/unit/notification-preview.test.ts`. + +- [ ] **Step 1:** In `src/lib/email.ts`, extract the template-resolution logic of `sendStyledNotificationEmail` into an exported pure function: +```ts +export function renderNotificationEmail( + name: string, + type: string, + context: NotificationEmailContext, + subjectOverride?: string, +): EmailTemplate { + const generator = NOTIFICATION_EMAIL_TEMPLATES[type] + const template = generator + ? generator({ ...context, name }) + : getNotificationEmailTemplate(name, subjectOverride || context.title, context.message, ensureAbsoluteUrl(context.linkUrl)) + return subjectOverride ? { ...template, subject: subjectOverride } : template +} +``` +Then refactor `sendStyledNotificationEmail` to call `renderNotificationEmail(...)` and `sendEmail(...)` the result (keep its existing signature/behavior identical — verify by re-running any notification email tests). + +- [ ] **Step 2:** In `src/server/routers/notification.ts`, hoist the `sampleData` map (currently inside `sendTestEmail`) to a module-level `const NOTIFICATION_SAMPLE_DATA` and ADD logistics entries so previews/tests are realistic: +```ts +FINALIST_CONFIRMED: { projectTitle: 'Ocean Cleanup Initiative', category: 'STARTUP' }, +FINALIST_DECLINED: { projectTitle: 'Ocean Cleanup Initiative', category: 'STARTUP' }, +FINALIST_EXPIRED: { projectTitle: 'Ocean Cleanup Initiative', category: 'STARTUP' }, +FINALIST_WAITLIST_PROMOTED:{ projectTitle: 'Reef Guardians', category: 'STARTUP' }, +FINALIST_REMINDER: { projectTitle: 'Ocean Cleanup Initiative', deadline: new Date(Date.now()+86_400_000).toISOString() }, +FINALIST_WITHDRAWN: { projectTitle: 'Ocean Cleanup Initiative', reason: 'Schedule conflict' }, +TRAVEL_CONFIRMED: { projectTitle: 'Ocean Cleanup Initiative', arrivalAt: new Date(Date.now()+5*86_400_000).toISOString(), arrivalFlightNumber: 'AF1234', arrivalAirport: 'NCE', departureAt: new Date(Date.now()+7*86_400_000).toISOString(), departureFlightNumber: 'AF1235', departureAirport: 'NCE', hotel: { name: 'Hotel de Paris', address: 'Place du Casino, Monaco', link: 'https://example.com' } }, +VISA_STATUS_UPDATE: { projectTitle: 'Ocean Cleanup Initiative', status: 'GRANTED' }, +``` +Have `sendTestEmail` use `NOTIFICATION_SAMPLE_DATA` (behavior unchanged otherwise). + +- [ ] **Step 3:** Add a `previewEmailTemplate` adminProcedure query: +```ts +previewEmailTemplate: adminProcedure + .input(z.object({ notificationType: z.string() })) + .query(async ({ ctx, input }) => { + const setting = await ctx.prisma.notificationEmailSetting.findUnique({ where: { notificationType: input.notificationType } }) + const label = setting?.label || input.notificationType + const metadata = NOTIFICATION_SAMPLE_DATA[input.notificationType] || {} + const rendered = renderNotificationEmail(ctx.user.name || 'Admin', input.notificationType, { + title: label, + message: `Preview of the "${label}" email.`, + linkUrl: `${process.env.NEXTAUTH_URL || ''}/applicant`, + linkLabel: 'Open', + metadata, + }, setting?.emailSubject || undefined) + return { subject: rendered.subject, html: rendered.html, hasStyledTemplate: input.notificationType in NOTIFICATION_EMAIL_TEMPLATES } + }), +``` +Import `renderNotificationEmail` from `@/lib/email`. + +- [ ] **Step 4: Test** (`tests/unit/notification-preview.test.ts`): call `notification.previewEmailTemplate({ notificationType: 'VISA_STATUS_UPDATE' })` via an admin caller; assert `html` contains a recognizable string (e.g. 'visa' or 'Grand Finale') and `subject` is non-empty. Also test a fallback type (`FINALIST_EXPIRED`) returns non-empty `html`. (Pattern: `createCaller(notificationRouter, {SUPER_ADMIN})`.) +- [ ] **Step 5:** `npx vitest run tests/unit/notification-preview.test.ts` → pass. `npm run typecheck` → clean. +- [ ] **Step 6: Commit** — `git commit -am "feat(notifications): renderNotificationEmail + previewEmailTemplate + logistics sample data"` + +--- + +## Task 2: UI — logistics Email Templates tab + show logistics in global settings + +**Files:** create `src/components/admin/logistics/email-templates-tab.tsx`; modify `src/components/settings/notification-settings-form.tsx`. + +- [ ] **Step 1:** Add `logistics: { label: 'Logistics', icon: Plane }` to the `CATEGORIES` map in `notification-settings-form.tsx` (import `Plane` from lucide-react) so logistics settings are also manageable on the global settings page. + +- [ ] **Step 2:** Build `EmailTemplatesTab` (`src/components/admin/logistics/email-templates-tab.tsx`), `'use client'`, mirroring `NotificationSettingsForm`'s structure but: (a) filter `trpc.notification.getEmailSettings` to `category === 'logistics'`; (b) per row show the toggle (`updateEmailSetting`), a **subject** input (debounced `onBlur` → `updateEmailSetting({ notificationType, sendEmail, emailSubject })`), a **Test** button (`sendTestEmail`), and a **Preview** button; (c) Preview opens `EmailPreviewDialog` with `previewOnly`, fetching `trpc.notification.previewEmailTemplate({ notificationType })` (lazy, `enabled: !!previewType`) and passing its `html` to `previewHtml`. Loading → Skeleton; empty → "No logistics email types found — run the notification settings seed." Use sonner toasts + `trpc.useUtils()` invalidation. + +- [ ] **Step 3:** `npm run typecheck` → clean. +- [ ] **Step 4: Commit** — `git commit -am "feat(logistics): Email Templates tab (toggle/subject/preview/test) + logistics in global settings"` + +--- + +## Task 3: Enable the tab in the logistics page + +**Files:** `src/app/(admin)/admin/logistics/page.tsx`. + +- [ ] **Step 1:** Remove `disabled` + the "(soon)" span from the `email-templates` `TabsTrigger`; import and add `` (the tab doesn't strictly need programId — settings are global — but pass it for consistency / future scoping; if unused, omit the prop). +- [ ] **Step 2:** `npm run typecheck` → clean. +- [ ] **Step 3: Commit** — `git commit -am "feat(logistics): enable Email Templates tab"` + +--- + +## Task 4: Verify + +- [ ] **Step 1:** `npx vitest run` — full suite green. +- [ ] **Step 2:** `npm run typecheck` — clean. Stop dev server, `rm -rf .next`, `npm run build` — clean. +- [ ] **Step 3:** Restart dev on :3001; dev smoke: `/admin/logistics` → Email Templates tab renders the 8 logistics types; toggle one off/on (persists); click Preview on `VISA_STATUS_UPDATE` → dialog shows the rendered branded email; click Test → success toast (email swallowed by localhost SMTP in dev — fine). Screenshot. +- [ ] **Step 4:** Summarize. + +## Notes +- No new email is sent automatically by this wave — it only adds admin visibility/control over the Wave-2 emails. +- Deferred to Wave 4: team-facing "My Logistics" + travel/visa UX fixes.