> ## 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/getting_started) (except the standalone `createProjectClient`).

## `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                   | Purpose                                                       |
| --------------------------- | ------------------------------------------------------------- |
| `ProjectHeader`             | Project title + editable project-level fields.                |
| `ProjectProgress`           | Milestone completion meter.                                   |
| `MilestoneJourney`          | Stepper across milestone `step_status`.                       |
| `TaskList` / `TaskRow`      | Ordered task rows.                                            |
| `StatusSelect`              | Editable status dropdown driven by a field's `options`.       |
| `StatusBadge` / `StatusDot` | Read-only status pill / colored dot.                          |
| `FieldValue`                | Renders one value according to its `ProjectFieldSchema` type. |

There's also a `formatFieldValue` helper that returns the display string for a value + schema (handy outside React).

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

function CustomProject() {
  const { project, milestones, tasks, tasksSchema, progress, canEdit, updateTask } = useProject()
  if (!project) return null
  return (
    <section>
      {progress && <ProjectProgress progress={progress} />}
      <MilestoneJourney milestones={milestones} />
      <TaskList
        tasks={tasks}
        schema={tasksSchema}
        canEdit={canEdit}
        onTaskUpdate={updateTask}
      />
    </section>
  )
}
```

## 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`).                           |

## 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/api_reference) for endpoints, headers, and error codes.
