# Project Template Gallery

**Date:** 2026-06-01
**Status:** Draft
**Spec:** 178
**Tier:** All tiers
**Depends on:** `projects-module`, `tasks-detail-communication`, `foundation-auth-rbac`
**Referenced by:** `projects-module`

---

## Overview

Project templates allow tenants to define reusable project structures — predefined tasks, phases, milestones, checklists, and billing configuration — that can be applied when creating a new project. Reduces setup time for recurring project types (e.g. "Website Redesign", "Monthly Retainer", "Brand Identity").

---

## Data Model

```sql
CREATE TABLE project_templates (
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id       UUID REFERENCES tenants(id) ON DELETE CASCADE,
    -- NULL = system template (global, read-only, available to all tenants)
  name            TEXT NOT NULL,
  description     TEXT,
  category        TEXT,                         -- 'design' | 'development' | 'marketing' | 'consulting' | 'other'
  estimated_days  INTEGER,                       -- typical project duration in days
  billing_type    TEXT,                          -- 'fixed' | 'hourly' | 'retainer' — pre-fill on project creation
  thumbnail_emoji TEXT DEFAULT '📋',             -- emoji for template card icon
  is_public       BOOLEAN NOT NULL DEFAULT false, -- if true + tenant-scoped: visible in tenant gallery
  usage_count     INTEGER NOT NULL DEFAULT 0,    -- times this template was used (for sorting)
  created_by      UUID REFERENCES users(id),
  created_at      TIMESTAMPTZ DEFAULT NOW(),
  updated_at      TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE project_template_tasks (
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  template_id     UUID NOT NULL REFERENCES project_templates(id) ON DELETE CASCADE,
  title           TEXT NOT NULL,
  description     TEXT,
  phase           TEXT,                          -- optional phase/group label
  position        INTEGER NOT NULL DEFAULT 0,   -- display order
  relative_start_days INTEGER,                  -- days after project start (for Gantt pre-fill)
  relative_due_days   INTEGER,                  -- days after project start
  is_milestone    BOOLEAN NOT NULL DEFAULT false,
  checklist_items JSONB DEFAULT '[]',            -- [{ "text": "...", "required": true }]
  estimated_hours NUMERIC(6,2),
  assigned_role   TEXT,                          -- role to auto-assign (MEMBER, ADMIN, CONTRACTOR)
  tags            TEXT[]
);

CREATE INDEX idx_template_tasks_template ON project_template_tasks(template_id, position);
```

---

## Features

### Template Gallery (`/projects/templates`)

Entry point: "New project" button → project creation wizard → "Start from template" step.

Also directly accessible at `/projects/templates` (OWNER/ADMIN only for management).

```
┌──────────────────────────────────────────────────────────────┐
│  Project Templates                           [+ New template] │
│                                                              │
│  Filter: [All ▾]  [Design ▾]   🔍 Search templates...        │
│                                                              │
│  ── System Templates ──────────────────────────────────────  │
│  ┌────────────┐  ┌────────────┐  ┌────────────┐             │
│  │ 🎨         │  │ 💻         │  │ 📣         │             │
│  │ Brand      │  │ Web App    │  │ Marketing  │             │
│  │ Identity   │  │ MVP        │  │ Campaign   │             │
│  │ 6 tasks    │  │ 12 tasks   │  │ 8 tasks    │             │
│  │ ~30 days   │  │ ~60 days   │  │ ~14 days   │             │
│  │ [Use] [Preview] │ [Use] [Preview] │ [Use] [Preview] │     │
│  └────────────┘  └────────────┘  └────────────┘             │
│                                                              │
│  ── My Templates ──────────────────────────────────────────  │
│  ┌────────────┐  ┌────────────┐                              │
│  │ 🖥️         │  │ 📋         │                              │
│  │ Client     │  │ Monthly    │                              │
│  │ Onboarding │  │ Retainer   │                              │
│  │ 4 tasks    │  │ 3 tasks    │                              │
│  │ [Use] [Edit] [Delete] │ [Use] [Edit] [Delete] │           │
│  └────────────┘  └────────────┘                              │
└──────────────────────────────────────────────────────────────┘
```

System templates (built-in, `tenant_id IS NULL`) are shown first, then tenant-created templates.

### Template Preview

Modal showing all tasks, phases, and estimated timeline before applying:

```
┌──────────────────────────────────────────────────────────────┐
│  Web App MVP                                 [Use template]  │
│  12 tasks · ~60 days                                  [✕]   │
│                                                              │
│  ── Phase 1: Discovery (Days 1-10) ───────────────────────── │
│  ☐ Kickoff meeting                                1d         │
│  ☐ Requirements gathering                         3d         │
│  ☐ Technical specification                        5d         │
│  📍 Phase 1 complete                              Day 10      │
│                                                              │
│  ── Phase 2: Design (Days 11-25) ─────────────────────────── │
│  ☐ Wireframes                                     4d         │
│  ☐ UI design (3 screens)                          7d         │
│  ☐ Client design review                           2d         │
│  📍 Design approved                               Day 25      │
│  ...                                                         │
└──────────────────────────────────────────────────────────────┘
```

### Apply template to new project

When "Use template" is clicked → new project creation form with template pre-filled:

1. Basic project fields (name, customer, billing type) — pre-filled from template
2. Start date input → all task dates computed as `start_date + relative_*_days`
3. Preview of generated tasks (editable before confirming)
4. [Create project] → creates project + all tasks in one transaction

```ts
// apps/zync-api/src/routes/projects/from-template.ts
export async function createProjectFromTemplate(
  templateId: string,
  projectData: CreateProjectInput,
  tenantId: string
) {
  const template = await getTemplate(templateId, tenantId)
  const tasks = await getTemplateTasks(templateId)

  // Compute absolute dates from relative days
  const projectStart = new Date(projectData.start_date)

  return await db.transaction(async (tx) => {
    const project = await createProject(tx, projectData)

    for (const templateTask of tasks) {
      const startDate = templateTask.relative_start_days != null
        ? addDays(projectStart, templateTask.relative_start_days)
        : null
      const dueDate = templateTask.relative_due_days != null
        ? addDays(projectStart, templateTask.relative_due_days)
        : null

      // assigned_role → concrete assignee: first active member holding that role
      // (null if the project has no member with the role — task is left unassigned)
      const assigneeId = templateTask.assigned_role
        ? await resolveFirstMemberWithRole(tx, tenantId, templateTask.assigned_role)
        : null

      const task = await createTask(tx, {
        project_id: project.id,
        title: templateTask.title,
        description: templateTask.description,
        phase: templateTask.phase,          // stored on tasks.phase (group/section label)
        start_date: startDate,
        due_date: dueDate,
        is_milestone: templateTask.is_milestone,
        checklist_items: templateTask.checklist_items,  // tasks.checklist_items (schema delta below)
        estimated_hours: templateTask.estimated_hours,
        assignee_id: assigneeId,
        position: templateTask.position,
        tenant_id: tenantId,
      })

      // tags → task_labels rows (labels live in task_labels, not a column on tasks)
      for (const tag of templateTask.tags ?? []) {
        await upsertTaskLabel(tx, { tenant_id: tenantId, task_id: task.id, label: tag })
      }
    }

    await incrementTemplateUsage(templateId)
    return project
  })
}
```

**Schema delta on `tasks`** (so applied template tasks have a home for every template field):
```sql
ALTER TABLE tasks ADD COLUMN checklist_items JSONB DEFAULT '[]';  -- [{ "text": "...", "required": bool, "done": bool }]
ALTER TABLE tasks ADD COLUMN phase TEXT;                           -- optional phase/section label (from template)
```
`checklist_items` renders as an inline checklist on task detail (spec 12); `phase` groups tasks under a section header in list/Gantt views. `tags` map to `task_labels` rows (spec 11) and `assigned_role` resolves to a concrete `assignee_id` at apply time — neither is a new column on `tasks`.

### Save project as template

"Save as template" action on existing project detail:

1. Extracts task structure (title, description, phase, position, estimated_hours, checklist_items)
2. Converts absolute dates to relative days from project start
3. Creates `project_templates` + `project_template_tasks` rows
4. Template appears in "My Templates" gallery

---

## System Templates

Built-in templates (seeded, `tenant_id IS NULL`):

| Name | Category | Tasks | Duration |
|------|----------|-------|----------|
| Brand Identity | design | 8 | 30 days |
| Web App MVP | development | 12 | 60 days |
| Marketing Campaign | marketing | 8 | 14 days |
| Website Redesign | design | 10 | 45 days |
| Monthly Retainer | consulting | 3 | recurring |
| Product Launch | marketing | 15 | 90 days |
| Client Onboarding | consulting | 5 | 7 days |

System templates are read-only (cannot edit/delete from tenant settings). Stored in `project_templates` with `tenant_id = NULL`.

---

## API

```
GET    /api/project-templates                  → list templates (system + tenant's own)
POST   /api/project-templates                  → create template from scratch
GET    /api/project-templates/:id              → template detail + tasks
PATCH  /api/project-templates/:id              → edit template (tenant-owned only)
DELETE /api/project-templates/:id              → delete (tenant-owned only)

GET    /api/project-templates/:id/tasks        → list template tasks
POST   /api/project-templates/:id/tasks        → add task
PATCH  /api/project-templates/:id/tasks/:tid   → edit task
DELETE /api/project-templates/:id/tasks/:tid   → remove task

POST   /api/projects/:id/save-as-template      → save existing project as template
       body: { name, description, category }
       Requires: projects:write

POST   /api/projects/from-template             → create project from template
       body: { template_id, project: { name, customer_id, start_date, billing_type, ... } }
       Requires: projects:write
```

---

## Architecture Decisions

| Decision | Choice | Reason |
|----------|--------|--------|
| `tenant_id IS NULL` for system templates | Not separate table | Simple filtering: `WHERE tenant_id = :tenantId OR tenant_id IS NULL`; system templates auto-available to all tenants |
| Relative dates in templates | Not absolute dates | Templates are reusable across different start dates; relative days allow correct date placement on any project start |
| Tasks created at project creation | Not synced after | Template is a starting point; tasks are owned by the project after creation; divergence from template is expected and desired |
| Save project as template | Extracts current state | Capturing a working project as a template ensures the template reflects a real workflow; simpler than designing templates from scratch |
| System templates seeded | Not user-contributed marketplace | Marketplace requires curation, versioning, and community moderation — deferred to future; curated system templates cover common IL freelancer use cases |
