🔴 Context Requirement: When creating a new module (Funerals, Baptisms, Presentations, etc.), you MUST include this file in your context BEFORE starting. This checklist ensures you don't miss critical steps and follow the correct order of implementation.
This checklist provides a comprehensive guide for creating new modules (Funerals, Baptisms, Presentations, etc.) in Outward Sign.
Reference Implementation: Wedding module (src/app/(main)/weddings/)
For detailed component patterns and code examples, see MODULE_COMPONENT_PATTERNS.md.
When creating a new module, follow these major steps:
- Database Layer - Migration, RLS policies, base types
- Server Actions - CRUD operations with
WithRelationsinterface - Module Structure - 9 main files + 1 print page (follow wedding pattern exactly)
- Reusable Components - Use existing pickers and shared components
- Content & Export - Content builder + API routes for PDF/Word
- Constants - Status constants and sidebar navigation
-
Create migration file in
supabase/migrations/- Create table with required fields (id, parish_id, status, created_at, updated_at, etc.)
- Add foreign key constraints (person_id, event_id, etc.)
- Add template_id field (e.g.,
wedding_template_id,funeral_template_id) - Create RLS policies for each role (staff, admin, parishioner)
- Grant appropriate permissions
-
Run migration
- Test migration locally
- Prompt user to run
supabase db push
- Create server actions file at
src/lib/actions/[entity].tsor[entities].ts- Define base
[Entity]interface - Define
[Entity]WithRelationsinterface extending base type - Implement
get[Entities](filters?)- List with server-side filtering - Implement
get[Entity](id)- Fetch single entity (basic) - Implement
get[Entity]WithRelations(id)- Fetch with all relations using Promise.all() - Implement
create[Entity](data)with revalidatePath - Implement
update[Entity](id, data)using Object.fromEntries pattern - Implement
delete[Entity](id)with revalidatePath - Define
Create[Entity]DataandUpdate[Entity]Datatypes
- Define base
Follow the wedding module pattern exactly for all 9 main files:
-
List Page -
app/(main)/[entities]/page.tsx(Server)- Auth check with redirect
- Fetch entities with filters from searchParams
- Compute stats server-side
- Define breadcrumbs
- Render:
BreadcrumbSetter→[Entity]ListClient
-
List Client -
app/(main)/[entities]/[entities]-list-client.tsx(Client, plural naming)- Accept
initialDataandstatsprops - Implement URL-based filters (search, status, etc.)
- Use
router.push()to update URL params (no client-side filtering) - Render: Search/Filters Card → Grid of entity cards → Empty state → Stats
- Card Button Placement (CRITICAL):
- Edit icon in upper right corner of card (Pencil icon, icon-only button)
- View button in bottom right corner of card (outlined button with text)
- Maintain consistent spacing and alignment across all modules
- Accept
-
Create Page -
app/(main)/[entities]/create/page.tsx(Server)- Auth check
- Define breadcrumbs
- Render:
BreadcrumbSetter→[Entity]FormWrapper(no entity prop)
-
View Page -
app/(main)/[entities]/[id]/page.tsx(Server)- Auth check
- Fetch entity using
get[Entity]WithRelations(id) - Define breadcrumbs
- Render:
PageContainer→BreadcrumbSetter→[Entity]ViewClient
-
Edit Page -
app/(main)/[entities]/[id]/edit/page.tsx(Server)- Auth check
- Fetch entity using
get[Entity]WithRelations(id) - Define breadcrumbs
- Render:
BreadcrumbSetter→[Entity]FormWrapper entity={entity}
-
Form Wrapper -
app/(main)/[entities]/[entity]-form-wrapper.tsx(Client, REQUIRED)- Accept optional
entity?: [Entity]WithRelationsprop - Wrap form with
PageContainer - Show action buttons in edit mode (View button + Save button at top)
- Manage loading state
- Pass props to
[Entity]Form
- Accept optional
-
Unified Form -
app/(main)/[entities]/[entity]-form.tsx(Client)- Accept optional
entity?: [Entity]WithRelationsprop - Compute
isEditing = !!entityat top - Use
isEditingfor all mode detection - CRITICAL: ALL inputs, selects, and textareas MUST use
FormFieldcomponent - Implement form fields with pickers (PeoplePicker, EventPicker, etc.)
- Use
openToNewEvent={!value}for EventPicker (e.g.,openToNewEvent={!weddingEvent.value}) - Use
openToNewPerson={!value}for PeoplePicker (e.g.,openToNewPerson={!presider.value}) - Place SaveButton and CancelButton at bottom
- Call
create[Entity]()orupdate[Entity]() - After UPDATE:
router.refresh()(stay on edit page) - After CREATE:
router.push(\/[entities]/${newEntity.id}/edit`)` (go to edit page)
- Accept optional
-
View Client -
app/(main)/[entities]/[id]/[entity]-view-client.tsx(Client)- Accept
entity: [Entity]WithRelationsprop - Use
ModuleViewContainercomponent - Implement
generateFilename()function - Implement
getTemplateId()function (read fromentity.[entity]_template_id) - Pass
buildLiturgycallback - Pass
onDelete={delete[Entity]}to ModuleViewContainer for delete functionality
- Accept
-
Print Page -
app/print/[entities]/[id]/page.tsx(Server)- Directory name must be PLURAL
- Fetch entity with relations
- Build liturgy using template ID from database
- Render HTML using
renderHTML() - Use print-specific styling
-
PDF Export Route -
app/api/[entities]/[id]/pdf/route.ts- Use
createPdfRoute()factory from@/lib/api/document-routes - Provide entity name, fetch function, build function, filename function
- Specify default template and template ID field name
- See wedding module for reference implementation
- Use
-
Word Export Route -
app/api/[entities]/[id]/word/route.ts- Use
createWordRoute()factory from@/lib/api/document-routes - Provide entity name, fetch function, build function, filename function
- Specify default template and template ID field name
- See wedding module for reference implementation
- Use
- Create content builder at
src/lib/content-builders/[entity]/index.ts- Export
build[Entity]Liturgy(entity, templateId)function - Create template files for different liturgy formats
- Return structured document array (sections, headings, paragraphs)
- Ensure single source of truth for all output formats
- Export
-
Add constants to constants file
- Create
[ENTITY]_STATUS_VALUESarray - Create
[ENTITY]_STATUS_LABELSobject with en/es translations - Export constants for use in forms and lists
- Create
-
Update sidebar navigation in
src/components/main-sidebar.tsx- Add module to sidebar with appropriate icon
- Use consistent icon from lucide-react (see Module Icons section)
-
Add loading states
-
app/(main)/[entities]/loading.tsx(imports reusable component) -
app/(main)/[entities]/[id]/loading.tsx(imports reusable component)
-
-
Add error boundaries
-
app/(main)/[entities]/error.tsx(imports reusable component) -
app/(main)/[entities]/[id]/error.tsx(imports reusable component)
-
Use the validation checklist below to verify your implementation.
ALWAYS create [entity]-form-wrapper.tsx - this is NOT optional
- Wraps the form with PageContainer
- Provides action buttons for edit mode (View + Save at top)
- Manages loading state
- ✅ CORRECT: After UPDATE →
router.refresh()(stays on edit page) - ✅ CORRECT: After CREATE →
router.push(\/[entities]/${newEntity.id}/edit`)` (goes to edit page) - ❌ WRONG: Using
router.push()after update (loses unsaved context) - ❌ WRONG: Staying on create page after creation
- ✅ Form must accept
[Entity]WithRelationstype (not base[Entity]) - ✅ View pages must fetch using
get[Entity]WithRelations(id) - ❌ Using base type causes missing nested data (people, events, readings)
- ✅ View client must use:
const templateId = entity.[entity]_template_id || 'default-template-id' - ✅ Pass templateId to:
build[Entity]Liturgy(entity, templateId) - ❌ Hard-coding template ID prevents users from selecting different templates
- Always create ALL files that exist in the wedding module
- Use exact same naming patterns (
[entities]-list-client.tsxwith plural) - Follow exact same component structure and props
CRITICAL: ALL form inputs, selects, and textareas MUST use the FormField component.
- ✅ CORRECT:
<FormField id="name" label="Name" value={name} onChange={setName} /> - ❌ WRONG:
<Label>Name</Label><Input value={name} onChange={(e) => setName(e.target.value)} /> - ❌ WRONG: Using bare
<Input />,<Select />, or<Textarea />components
Why: FormField provides consistent styling, labels, descriptions, and layout across all forms.
Exceptions:
- Picker components (PeoplePicker, EventPicker) have their own internal structure
- If you need to use a bare input/select/textarea, ALWAYS ask the user first
Before completing a new module, verify:
- Form wrapper exists and matches wedding pattern
- FormField component is used for ALL inputs, selects, and textareas
- Redirections match wedding module (refresh on update, push on create)
- Types use
WithRelationsinterfaces - Template ID is read from database and used in liturgy builder
- All view pages (view, print, PDF, Word) use template ID from database
- Icon is consistent across all uses
- All wedding module files have corresponding files in new module
- RLS policies are properly configured for all roles
- Server actions include all CRUD operations
- Breadcrumbs are set on all pages
- Loading and error states are implemented
- Constants are added with bilingual labels
- Sidebar navigation includes the new module
Each module must use a consistent icon from lucide-react throughout the application:
- Weddings:
VenusAndMars - Funerals:
Cross - Baptisms: TBD
- Presentations: TBD
The main sidebar (src/components/main-sidebar.tsx) is the source of truth for which icon should be used for each module.
Icons are used in:
- Main sidebar navigation
- Module list pages (if displaying icons)
- Breadcrumbs or page headers (if applicable)