> ## Documentation Index
> Fetch the complete documentation index at: https://help.statisfy.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Project Module

> Show and update a customer's onboarding project with ProjectModule, the headless useProject hook, composable primitives, and the project client.

The Project module renders a customer's onboarding **project** — its milestones, tasks, progress, and editable fields — and lets the customer update them. Use the pre-assembled component, drop to the headless hook, or compose your own layout from primitives.

Everything here reads connection + auth from [`<StatisfyProvider>`](/sdk/v0/getting_started) (except the standalone `createProjectClient`).

<Warning>
  The project endpoints require a **portal-session** token — one carrying a `customer_id` claim, which only the `POST /sdk/auth` exchange produces. If your backend mints the bearer itself (the vendored path), these calls return `401 not_a_portal_session`; the Digital Worker chat still works. See [Authentication](/sdk/v0/api_reference#authentication).
</Warning>

## `ProjectModule`

The pre-assembled, ready-to-render project view.

```tsx theme={null}
import { ProjectModule } from '@statisfy/digital-workers-react'

// Auto-resolves the signed-in customer's onboarding project.
<ProjectModule className="dw-max-w-3xl" />

// Or render a specific project by id.
<ProjectModule projectId="1102" />
```

| Prop        | Type     | Default             | Description                                                                                                                |
| ----------- | -------- | ------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `projectId` | `string` | resolved via config | Project to render. When omitted, the module fetches `GET /sdk/v1/config/portal` and uses the account's onboarding project. |
| `className` | `string` | —                   | Extra classes on the root card (width/height constraints).                                                                 |

It renders a header, a progress meter, a milestone journey, and the task list — and writes edits back through the gateway, scoped to the session's customer.

## Headless: `useProject`

For full control of the layout, use the hook. It resolves the project (by id, or via config when you omit the id), exposes the data, and provides optimistic mutations.

```tsx theme={null}
import { useProject } from '@statisfy/digital-workers-react'

function MyProjectView() {
  const {
    project, tasks, milestones, tasksByParent,
    schema, tasksSchema, progress, canEdit,
    loading, error,
    updateTask, updateProject, reload
  } = useProject() // pass a projectId to skip config resolution

  if (loading) return <Spinner />
  if (error) return <button onClick={reload}>Retry ({error.message})</button>
  if (!project) return null

  return (
    <div>
      <h2>{project.display.title}</h2>
      {progress && <p>{progress.completed}/{progress.total} complete</p>}
      {tasks.map((t) => (
        <label key={t.id}>
          {t.display.title}
          {canEdit && (
            <input
              type="checkbox"
              checked={t.status === 'COMPLETED'}
              onChange={(e) =>
                updateTask(t.id, { status: e.target.checked ? 'COMPLETED' : 'TODO' })
              }
            />
          )}
        </label>
      ))}
    </div>
  )
}
```

### `UseProjectResult`

| Field           | Type                                    | Description                                                                               |
| --------------- | --------------------------------------- | ----------------------------------------------------------------------------------------- |
| `project`       | `ProjectData \| null`                   | The project payload, or `null` while loading / unresolved.                                |
| `tasks`         | `ProjectTask[]`                         | Flat, server-ordered task list (never re-sorted).                                         |
| `milestones`    | `ProjectTask[]`                         | Root-level milestones (the journey), in server order.                                     |
| `tasksByParent` | `Map<string \| null, ProjectTask[]>`    | Tasks grouped by `parent_id` (`null` = root), order preserved.                            |
| `schema`        | `ProjectFieldSchema[]`                  | Project-level field definitions.                                                          |
| `tasksSchema`   | `ProjectFieldSchema[]`                  | Shared task-row field definitions.                                                        |
| `progress`      | `ProjectProgress \| null`               | Server-computed meter, or `null` to hide it.                                              |
| `canEdit`       | `boolean`                               | Whether the session may edit (combine with each field's `editable`).                      |
| `loading`       | `boolean`                               | True during the initial resolve / reload.                                                 |
| `error`         | `DigitalWorkerError \| null`            | Set on load failure (401/404/network).                                                    |
| `updateTask`    | `(taskId, properties) => Promise<void>` | Optimistically update a task; re-renders from the PATCH response and rolls back on error. |
| `updateProject` | `(properties) => Promise<void>`         | Same contract, for project-level fields.                                                  |
| `reload`        | `() => void`                            | Re-run the resolve (e.g. retry after an error).                                           |

<Note>
  `updateTask` / `updateProject` apply your change **optimistically** (the UI updates immediately), then reconcile with the server's full response — and roll the row back if the request fails. They reject on failure so you can show an inline error.
</Note>

<Warning>
  Send only the fields you're changing (a **diff**), and only **editable** fields. A non-editable or unknown key is rejected with `400 not_editable`; a bad value with `400 invalid_option` / `400 invalid_type`.
</Warning>

## Composable primitives

Build a bespoke layout from the same building blocks `ProjectModule` uses. Each is schema-driven and styled with the SDK's theme.

| Component          | Props                                                                                 | Purpose                                                                                                                   |
| ------------------ | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `ProjectHeader`    | `project`, `className?`                                                               | Project title + editable project-level fields.                                                                            |
| `ProjectProgress`  | `progress`, `className?`                                                              | Milestone completion meter. Renders nothing when `progress` is `null` or its `percent` is `null`.                         |
| `MilestoneJourney` | `milestones`, `tasksSchema`, `className?`                                             | Stepper across milestone `step_status`. Renders nothing when `milestones` is empty.                                       |
| `TaskList`         | `milestones`, `tasksByParent`, `tasksSchema`, `canEdit`, `onSelectTask`, `className?` | Ordered task rows, grouped under their milestone.                                                                         |
| `TaskRow`          | `task`, `tasksSchema`, `canEdit`, `onSelect`                                          | One task row. `onSelect` takes no arguments — bind the task in the parent.                                                |
| `TaskDetail`       | `projectId`, `task`, `tasksSchema`, `canEdit`, `onUpdate`, `onClose`                  | Task detail drawer, including its comment thread. `onUpdate(properties)` receives a canonical diff and returns a promise. |
| `StatusSelect`     | `schema`, `value`, `onChange`, `disabled?`, `className?`, `aria-label?`               | Editable status dropdown driven by a field's `options`. `onChange` receives the option `value`.                           |
| `StatusBadge`      | `option?`, `label?`, `className?`                                                     | Read-only status pill (colored dot + label).                                                                              |
| `StatusDot`        | `color?`, `className?`                                                                | Colored dot only; omit `color` for the neutral fallback.                                                                  |
| `FieldValue`       | `schema`, `properties`, `display`, `className?`                                       | Renders one value according to its `ProjectFieldSchema` type.                                                             |
| `Comments`         | `projectId`, `taskId?`, `title?`, `className?`                                        | Comment thread. With `taskId` it scopes to that task; without, the project thread.                                        |

Two non-component helpers round it out:

* `formatFieldValue(schema, properties, display)` — the display string for one field, usable outside React.
* `useComments(projectId, taskId?)` — the headless hook behind `Comments`, returning `{ comments, loading, error, posting, addComment, reload }`.

<Warning>
  These props are **positional in meaning, not interchangeable**: `TaskList` takes `milestones` + `tasksByParent` (not a flat `tasks` array) and `onSelectTask` (not an update callback) — it selects a task, it doesn't write. Writes go through `useProject`'s `updateTask`.
</Warning>

```tsx theme={null}
import {
  useProject, ProjectHeader, ProjectProgress, MilestoneJourney, TaskList, TaskDetail,
  type ProjectTask
} from '@statisfy/digital-workers-react'
import { useState } from 'react'

function CustomProject() {
  const {
    project, milestones, tasksByParent, tasksSchema, progress, canEdit, updateTask
  } = useProject()
  const [selected, setSelected] = useState<ProjectTask | null>(null)

  if (!project) return null

  return (
    <section>
      <ProjectHeader project={project} />
      <ProjectProgress progress={progress} />
      <MilestoneJourney milestones={milestones} tasksSchema={tasksSchema} />
      <TaskList
        milestones={milestones}
        tasksByParent={tasksByParent}
        tasksSchema={tasksSchema}
        canEdit={canEdit}
        onSelectTask={setSelected}
      />
      {selected && (
        <TaskDetail
          projectId={project.id}
          task={selected}
          tasksSchema={tasksSchema}
          canEdit={canEdit}
          onUpdate={(properties) => updateTask(selected.id, properties)}
          onClose={() => setSelected(null)}
        />
      )}
    </section>
  )
}
```

## Comments

Drop a comment thread onto a project or a single task. `TaskDetail` already includes one; use `Comments` directly when you're composing your own layout.

```tsx theme={null}
import { Comments, useComments } from '@statisfy/digital-workers-react'

// Pre-built thread: project-level, or task-scoped with taskId.
<Comments projectId={project.id} />
<Comments projectId={project.id} taskId={task.id} title="Task notes" />

// Or drive your own UI with the hook.
function MyThread({ projectId }: { projectId: string }) {
  const { comments, loading, error, posting, addComment, reload } = useComments(projectId)
  if (error) return <button onClick={reload}>Retry ({error.message})</button>
  return (
    <ul>
      {comments.map((c) => <li key={c.id}>{c.value}</li>)}
      <button disabled={posting || loading} onClick={() => addComment('Looks good!')}>
        Comment
      </button>
    </ul>
  )
}
```

Comments come back in server order (oldest first). `addComment` appends the server row on success and rejects on failure, so you can surface an inline error.

## Standalone client: `createProjectClient`

When you want project data outside React (or your own state layer), use the client directly. It doesn't need the provider — pass config explicitly.

```ts theme={null}
import { createProjectClient } from '@statisfy/digital-workers-react'

const projects = createProjectClient({
  baseUrl: 'https://api.statisfy.app',
  getToken: async () => sessionToken,
  publishableKey: 'pk_live_your_key'
})

// Resolve the onboarding project id for the current customer
const config = await projects.getConfig('portal')
const projectId = String(config.data.onboarding_project__c_)

const { data, meta } = await projects.getProject(projectId)
console.log(data.display.title, meta.can_edit)

// Diff-update a task's status
await projects.patchTask(projectId, 'task-uuid', { status: 'COMPLETED' })
```

| Method                              | Endpoint                                             | Description                                                           |
| ----------------------------------- | ---------------------------------------------------- | --------------------------------------------------------------------- |
| `getConfig(module)`                 | `GET /sdk/v1/config/{module}`                        | Resolve a module's config (e.g. `portal` → onboarding project id).    |
| `getProject(id)`                    | `GET /sdk/v1/projects/{id}`                          | Read a project: values + display + schema + ordered tasks + progress. |
| `patchProject(id, properties)`      | `PATCH /sdk/v1/projects/{id}`                        | Diff-update project fields.                                           |
| `patchTask(id, taskId, properties)` | `PATCH /sdk/v1/projects/{id}/tasks/{taskId}`         | Diff-update a task's fields (v0: `status`).                           |
| `getProjectComments(id)`            | `GET /sdk/v1/projects/{id}/comments`                 | Project-level comments visible to the session.                        |
| `addProjectComment(id, value)`      | `POST /sdk/v1/projects/{id}/comments`                | Post a project-level comment.                                         |
| `getTaskComments(id, taskId)`       | `GET /sdk/v1/projects/{id}/tasks/{taskId}/comments`  | Comments on one task.                                                 |
| `addTaskComment(id, taskId, value)` | `POST /sdk/v1/projects/{id}/tasks/{taskId}/comments` | Post a comment on a task.                                             |

<Note>
  Comments posted through the SDK are always **customer-visible** (`external`). Editing a comment is REST-only — `PATCH /sdk/v1/projects/{id}/comments/{commentId}`, author-only — and has no client method in v0.
</Note>

## Data shapes

```ts theme={null}
interface ProjectData {
  id: string
  properties: Record<string, unknown>   // canonical values
  display: Record<string, string>       // formatted strings for rendering
  schema: ProjectFieldSchema[]          // project-level fields
  progress: ProjectProgress | null
  tasks: ProjectTask[]
  tasks_schema: ProjectFieldSchema[]    // shared task-row fields
}

interface ProjectTask {
  id: string
  parent_id: string | null
  is_milestone: boolean
  status: string | null
  step_status: 'done' | 'current' | 'upcoming' | string
  properties: Record<string, unknown>
  display: Record<string, string>
}

interface ProjectProgress { percent: number | null; completed: number; total: number }
interface ProjectFieldSchema { key: string; label: string; type: ProjectFieldType; editable: boolean; options?: ProjectSelectOption[] }
```

See the [API Reference](/sdk/v0/api_reference) for endpoints, headers, and error codes.
