# Home Dashboard

**Date:** 2026-05-31
**Status:** Draft
**Depends on:** `app-shell`, `foundation-auth-rbac`, `foundation-design-system`, `tasks-board-engine`, `invoices-core`, `projects-module`, `customers-module`, `calendar-module`, `time-management`
**Referenced by:** `app-shell` (default redirect target), `tenant-portals`

---

## Overview

The home dashboard is the first screen a user sees after login. It lives at `/` (the shell redirects here by default). It is an ambient awareness surface — not an analytics tool. It answers: "What is happening right now, what needs my attention today, and what should I do next?"

This spec covers only the home dashboard at `/`. The custom analytics dashboards at `/reports/analytics` are a separate feature covered in `reports-analytics.md`.

The dashboard is fully module-aware: every section, card, and action silently hides itself when its backing module is disabled for the tenant. There are no "N/A" states, no locked placeholders, no upgrade prompts on this screen.

**Module availability** is determined by the `tenant_modules` table (spec 32 — `2026-05-31-module-management`). Each tenant independently enables or disables modules; all 14 toggleable modules default to enabled on signup. The `modules` map in the API response is derived server-side by reading the tenant's `tenant_modules` rows. Tier gating (`useTierGate` / `requireTier`) is orthogonal — it controls feature access by subscription level but does not affect per-tenant module state.

---

## Layout

12-column grid. Not centered. Content left-aligned within the page container. Headlines sit on odd column spans. Negative space is intentional — the grid should breathe. No bento boxes.

Page sections stack vertically:

1. Quick Actions bar
2. KPI row
3. Two-column content area: Activity feed (left 8 cols) + Upcoming sidebar (right 4 cols)
4. Setup checklist (conditional, full width)

### ASCII Wireframe

```
┌─────────────────────────────────────────────────────────────────────────────┐
│ SHELL HEADER (app-shell)                                                    │
├──────────┬──────────────────────────────────────────────────────────────────┤
│          │                                                                  │
│ SIDEBAR  │  [+ New Invoice]  [+ New Task]  [▶ Start Timer]  [+ Customer]   │
│          │  ───────────────────────────────────────────────────────────     │
│          │  ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌──────┐ │
│          │  │ Revenue │  │ Open    │  │ Active  │  │ Pending │  │Over- │ │
│          │  │ (month) │  │ Invoices│  │Projects │  │  Tasks  │  │ due  │ │
│          │  └─────────┘  └─────────┘  └─────────┘  └─────────┘  └──────┘ │
│          │                                                                  │
│          │  ┌────────────────────────────────────┐  ┌───────────────────┐  │
│          │  │                                    │  │                   │  │
│          │  │  Activity Feed                     │  │  Today            │  │
│          │  │  (8 cols)                          │  │  ─────────────    │  │
│          │  │                                    │  │  09:00 Team sync  │  │
│          │  │  Alex created task "Fix auth bug"  │  │  11:00 Client call│  │
│          │  │  2m ago                            │  │                   │  │
│          │  │                                    │  │  Tasks due today  │  │
│          │  │  Dana sent invoice INV-00091       │  │  ─────────────    │  │
│          │  │  14m ago                           │  │  ☐ Fix auth bug   │  │
│          │  │                                    │  │  ☐ Review spec    │  │
│          │  │  Payment received ₪4,800           │  │                   │  │
│          │  │  1h ago                            │  │  Tasks due tmrw   │  │
│          │  │                                    │  │  ─────────────    │  │
│          │  │  ...                               │  │  ☐ Deploy v2.1    │  │
│          │  │                                    │  │                   │  │
│          │  └────────────────────────────────────┘  └───────────────────┘  │
│          │                                            (4 cols)              │
│          │  ┌─────────────────────────────────────────────────────────┐    │
│          │  │  ✦ Setup checklist  [✕ dismiss]                         │    │
│          │  │  ☑ Add your business info                               │    │
│          │  │  ☐ Set up your first integration                        │    │
│          │  │  ☐ Invite a team member                                 │    │
│          │  │  ☐ Create your first customer                           │    │
│          │  │  ☐ Send your first invoice                              │    │
│          │  └─────────────────────────────────────────────────────────┘    │
└──────────┴──────────────────────────────────────────────────────────────────┘
```

---

## Sections

### 1. Quick Actions Bar

Horizontal strip, left-aligned, top of content area. Not full-width.

Actions rendered in this order, each as a secondary button with a leading icon:

| Action | Icon | Module required | Destination |
|--------|------|-----------------|-------------|
| New Invoice | Receipt | `invoices` | Open create-invoice sheet |
| New Task | CheckSquare | `tasks` | Open create-task modal |
| Start Timer | Play | `time` | Start timer (immediate, or open timer modal if no recent task context) |
| New Customer | UserPlus | `customers` | Open create-customer sheet |

- Actions whose module is disabled are not rendered (no hidden state, no tooltip).
- If all modules are disabled, the bar is not rendered.
- Buttons are not full-width. They sit flush left with the content grid, not stretched.
- On mobile (< 768px): horizontally scrollable row, no wrapping.

### 2. KPI Row

Five `StatCard` components in a flex row, wrapping on smaller viewports.

| Card | Label | Value | Secondary | Module | Query |
|------|-------|-------|-----------|--------|-------|
| Revenue this month | "Revenue this month" | Sum of `total` for invoices where `status = 'PAID'` and `paid_at` falls in the current calendar month | Currency formatted | `invoices` | See API |
| Open invoices | "Open invoices" | Count of invoices with `status IN ('SENT', 'APPROVED', 'TAX_ISSUED')` | Total outstanding amount (sum of `total`) | `invoices` | See API |
| Active projects | "Active projects" | Count of `projects` where `status = 'active'` | — | `projects` | See API |
| Pending tasks | "Pending tasks" | Count of tasks where `assignee_id = currentUser.id` and `status_name IN ('TODO', 'IN_PROGRESS')` (non-terminal statuses matching these names; uses `task_statuses.name` comparison) | — | `tasks` | See API |
| Overdue items | "Overdue" | Count of tasks past `due_date` (non-terminal) + count of invoices past `due_date` (status `SENT`, `APPROVED`, or `TAX_ISSUED`) | — | `tasks` or `invoices` (shown if either enabled) | See API |

**Rules:**
- Cards whose module is entirely disabled are not rendered. No "N/A", no placeholders.
- The Overdue card is shown if either `tasks` or `invoices` is enabled; its count reflects only the enabled modules.
- `loading={true}` prop on each `StatCard` during initial fetch (skeleton state from the design system).
- `StatCard` trend field is not used on this screen (no sparklines here — that belongs in analytics).
- Pending tasks count is always scoped to the current user's assignments regardless of role.
- CONTRACTOR role users: only see tasks assigned to them (their `tasks:read` is assignment-scoped by default). Revenue, Open invoices, and Active projects KPIs appear if the role has `invoices:read` / `projects:read` — CONTRACTOR does not have those by default, so those cards are hidden for CONTRACTOR users.
- MEMBER, VIEWER, ADMIN, OWNER: all three revenue/invoice/project KPI cards visible (these roles carry the relevant read permissions).

### 3. Activity Feed

**Position:** Left 8 columns of the two-column content area.

**Content:** Last 30 activity events, in reverse-chronological order (newest first), across all enabled modules.

**Event types:**

| Event type | Template | Entity link |
|------------|----------|-------------|
| `task.created` | `{actor} created task "{title}"` | `/tasks/{id}` |
| `task.completed` | `{actor} completed task "{title}"` | `/tasks/{id}` |
| `invoice.sent` | `{actor} sent invoice {invoiceNumber}` | `/invoices/{id}` |
| `invoice.paid` | `Payment received {amount} for {invoiceNumber}` | `/invoices/{id}` |
| `ticket.opened` | `{actor} opened ticket "{title}"` | `/support/{id}` |
| `project.started` | `{actor} started project "{name}"` | `/projects/{id}` |
| `customer.added` | `{actor} added customer "{name}"` | `/customers/{id}` |

- Events whose module is disabled are not included in the feed.
- `actor` is the display name of the user who performed the action. For system-generated events (e.g. automated invoice), actor is "Zync (auto)".
- Time displayed as relative (`2m ago`, `1h ago`, `Yesterday`, date string if older than 48h).
- Each row is clickable; clicking navigates to the entity.
- Activity events are filtered by the current user's module read permissions. A user without `invoices:read` does not see invoice events; a user without `projects:read` does not see project events. CONTRACTOR role sees only task events (their own tasks only). No special-cased role logic beyond the permission check.

**Row anatomy:**

```
[Avatar] [Actor name]  [Action verb + entity name]  [relative time]
```

Avatar: initials circle (design system `Avatar` primitive). 32px.

**Empty state:** If there are no events (new tenant, no activity yet), render the `EmptyState` primitive:
- Icon: ActivityIcon (or similar)
- Title: "Nothing happened yet."
- No action button.

**Loading state:** Render 6 skeleton rows (use design system skeleton variant of the feed row). No full-page spinner.

### 4. Upcoming Sidebar

**Position:** Right 4 columns of the two-column content area.

**Shown when:** At least one of `calendar` or `tasks` modules is enabled.

**Hidden entirely when:** Both `calendar` and `tasks` are disabled.

**Sub-sections:**

#### Today's Calendar Events
Shown only when `calendar` module is enabled.

Lists calendar events for today (from `calendar_events` where `start_time` is within the current calendar day for the user's timezone). Ordered by start time ascending.

Each row:
```
[time range]  [event title]
09:00–10:00   Team standup
```

Max 5 events shown. If more, show "View calendar →" link. If no events today, section omitted entirely (no empty state — just don't render).

#### Tasks Due Today
Shown only when `tasks` module is enabled.

Tasks where `due_date = today` and `status` is non-terminal, assigned to the current user.

Each row: checkbox (read-only visual, not interactive here) + task title, linked to the task.

#### Tasks Due Tomorrow
Same as above but `due_date = tomorrow`.

If both Tasks Due Today and Tasks Due Tomorrow have zero items, neither section is shown (not even a heading).

**Empty state for sidebar:** If calendar is enabled but has no events today AND tasks is enabled but has no due tasks today or tomorrow, render:
- Plain text: "You're all clear today." — no icon, no action.

### 5. Setup Checklist

**Visibility conditions (ALL must be true):**
1. Current user has ADMIN or OWNER role in the tenant.
2. The checklist has never been dismissed (`tenants.checklist_dismissed_at IS NULL`).
3. `tenants.onboarding_completed = false`.

If any condition fails, the checklist is not rendered at all.

**Placement:** Below the two-column content area, full content width.

**Items (5 fixed, in this order):**

| # | Label | Completion signal | Destination link |
|---|-------|-------------------|-----------------|
| 1 | Add your business info | `tenants.name` and `tenants.address` both non-null | `/settings/business` |
| 2 | Set up your first integration | At least one active integration connection for this tenant (table name to be confirmed against the integrations/settings spec — likely `integration_connections` or `calendar_connections`) | `/settings/integrations` |
| 3 | Invite a team member | `tenant_memberships` count > 1 (more than the owner) | `/settings/users` |
| 4 | Create your first customer | At least one `customers` row for this tenant | `/customers/new` |
| 5 | Send your first invoice | At least one invoice with `status != 'DRAFT'` | `/invoices/new` |

Each item: checkbox icon (checked/unchecked), label text, link arrow. Completed items show with a checked visual and reduced opacity but remain visible.

**Dismiss:** "✕" button top-right of the checklist panel. On click:
- Optimistic UI: checklist disappears immediately.
- `PATCH /api/dashboard/checklist/dismiss` — sets `tenants.checklist_dismissed_at = now()`.
- Once dismissed, the checklist never reappears (even after page refresh, even if conditions re-emerge).
- Does not set `onboarding_completed`. That field is set server-side when all 5 items are complete.

---

## Data Model

### `tenants` table delta (checklist dismiss)

```sql
ALTER TABLE tenants
  ADD COLUMN IF NOT EXISTS checklist_dismissed_at TIMESTAMPTZ;
```

`checklist_dismissed_at` is set once when an ADMIN/OWNER dismisses the setup checklist. A separate `setup_checklist` table is not needed — this is a single tenant-level flag with no per-user or per-role variance required. Keeping it on the `tenants` row avoids an extra table and a join on every dashboard load.

### `tenants` table delta

```sql
-- If not already present from foundation:
ALTER TABLE tenants ADD COLUMN IF NOT EXISTS onboarding_completed BOOLEAN NOT NULL DEFAULT false;
```

`onboarding_completed` is set to `true` server-side when all 5 checklist items pass their completion signals. Checked asynchronously after each relevant mutation (customer created, invoice sent, etc.) via a lightweight check function. Not re-evaluated on every dashboard load.

### Activity events source

The activity feed reads from the existing `audit_log` table (spec: `audit-compliance`). The dashboard API filters `audit_log` rows by `action` codes that map to the 7 event types in the feed. No new table.

The `audit_log` table stores `actor_id`, `entity_type`, and `entity_id` but does not store `entity_name` or `actor_name` as columns (see audit-compliance data model). The API handler must join to resolve display names:

- `actor_name` / `actor_initials`: join `users` on `actor_id` → `users.display_name`
- `entity_name`: join the entity's primary table on `entity_id` → the relevant name column (e.g. `tasks.title`, `invoices.proforma_number` or `invoice_number`, `projects.name`, `customers.name`, `tickets.title`)
- `amount` / `currency`: for `invoice.paid` events, join `invoices` to get `total` and `currency`

These joins are indexed lookups (primary key on entity tables) and are acceptable given the `LIMIT 30` result set. If feed latency becomes a concern, a denormalized `entity_name TEXT` column can be added to `audit_log` as a future optimization — the dashboard handler would then use it directly without joins.

The following `audit_log.action` values map to feed event types:

| audit_log.action | audit_log.entity_type | Feed event type |
|-----------------|-----------------------|----------------|
| `created` | `task` | task.created |
| `status_changed` (changes JSONB contains new status where `is_terminal = true`) | `task` | task.completed |
| `status_changed` (new status = `SENT`) | `invoice` | invoice.sent |
| `status_changed` (new status = `PAID`) | `invoice` | invoice.paid |
| `created` | `ticket` | ticket.opened |
| `created` | `project` | project.started |
| `created` | `customer` | customer.added |

> Note: the exact `action` string values used by each module when writing to `audit_log` must be confirmed against the respective module spec implementations. The table above represents the intended mapping; if action strings differ in practice, align at implementation time.

### Calendar events source

Reads from `calendar_events` table (spec: `calendar-module`). Dashboard query: events where `tenant_id = ?` AND `user_id = currentUser.id` AND `start_time >= today_start` AND `start_time < today_end` (user's local timezone applied server-side from `user_preferences.timezone`).

---

## API Endpoints

### `GET /api/dashboard`

Single endpoint. Returns all widget data in one call. No waterfall. Called once on mount.

**Auth:** Requires valid session. Response data scoped to tenant and user via session context.

**Response shape:**

```ts
interface DashboardResponse {
  kpis: {
    revenue_this_month: { amount: number; currency: string } | null        // null = invoices module disabled
    open_invoices: { count: number; outstanding: number; currency: string } | null
    active_projects: { count: number } | null                              // null = projects module disabled
    pending_tasks: { count: number } | null                                // null = tasks module disabled
    overdue: { tasks: number; invoices: number; total: number } | null     // null = both modules disabled
  }
  recent_activity: ActivityEvent[]
  upcoming: {
    events: CalendarEvent[]          // [] if calendar disabled or no events today
    tasks_today: TaskDue[]           // [] if tasks disabled or none due today
    tasks_tomorrow: TaskDue[]        // [] if tasks disabled or none due tomorrow
  }
  setup_checklist: {
    shown: boolean                   // false if dismissed, not admin, or onboarding_completed
    dismissed: boolean
    items: ChecklistItem[]
  } | null
  modules: {
    invoices: boolean
    tasks: boolean
    projects: boolean
    customers: boolean
    calendar: boolean
    time: boolean
  }
}

interface ActivityEvent {
  id: string
  type: 'task.created' | 'task.completed' | 'invoice.sent' | 'invoice.paid'
       | 'ticket.opened' | 'project.started' | 'customer.added'
  actor_id: string
  actor_name: string
  actor_initials: string
  entity_id: string
  entity_name: string          // task title, invoice number, project name, etc.
  entity_url: string           // e.g. "/tasks/abc123"
  amount?: number              // only for invoice.paid
  currency?: string
  occurred_at: string          // ISO 8601
}

interface CalendarEvent {
  id: string
  title: string
  start_time: string           // ISO 8601
  end_time: string
}

interface TaskDue {
  id: string
  title: string
  project_name: string | null
  is_terminal: boolean         // always false (only non-terminal tasks returned)
}

interface ChecklistItem {
  key: 'business_info' | 'integration' | 'team_member' | 'first_customer' | 'first_invoice'
  label: string
  completed: boolean
  link: string
}
```

**Server-side scoping:**
- KPI queries all scoped to `tenant_id` from session.
- `pending_tasks` further scoped to `assignee_id = currentUser.id`.
- Activity feed: filtered per caller's role permissions. CONTRACTOR — task events only, own tasks. MEMBER/VIEWER — task, project, customer, invoice events (per their read permissions). ADMIN/OWNER — full tenant feed.
- Calendar events: scoped to `user_id = currentUser.id`.
- Tasks due: scoped to `assignee_id = currentUser.id`.

**Performance:**
- All sub-queries run in parallel (Promise.all on the server).
- KPI queries use indexed columns only (`status`, `paid_at`, `due_date`, `assignee_id`).
- Activity feed: `LIMIT 30` on audit_log with index on `(tenant_id, occurred_at DESC)`.
- Target p95 response time: < 300ms.

**Caching:** No server-side cache. Client-side: React Query with `staleTime: 60_000` (1 minute). Manual invalidation on Quick Action completions (new invoice/task/customer created).

### `PATCH /api/dashboard/checklist/dismiss`

**Auth:** ADMIN or OWNER role required. 403 otherwise.

**Body:** none

**Action:** `UPDATE tenants SET checklist_dismissed_at = now() WHERE id = :tenantId`.

**Response:** `204 No Content`

---

## Permissions

| Action | Required |
|--------|----------|
| View dashboard | Any authenticated tenant user |
| See Revenue KPI | `invoices:read` |
| See Open Invoices KPI | `invoices:read` |
| See Active Projects KPI | `projects:read` |
| See Pending Tasks KPI | `tasks:read` (scoped to own tasks) |
| See Overdue KPI | `invoices:read` OR `tasks:read` |
| See activity feed | Aggregated from individual module read permissions |
| See upcoming calendar events | `calendar:read` (own events only) |
| See upcoming tasks | `tasks:read` (own tasks only) |
| See setup checklist | ADMIN or OWNER role |
| Dismiss setup checklist | ADMIN or OWNER role |

System roles relevant to the dashboard (from `foundation-auth-rbac`):

| Role | Dashboard access |
|------|-----------------|
| OWNER / ADMIN | All KPIs, full activity feed, setup checklist visible |
| MEMBER | All KPIs (has `tasks:read`, `projects:read`, `invoices:read`). Activity feed filtered per permissions. No setup checklist. |
| VIEWER | Same as MEMBER (read-only across all modules). |
| CONTRACTOR | Pending tasks KPI only (own tasks). Activity feed: own task events only. No revenue/project/invoice KPIs. No setup checklist. |

No "STAFF" role exists in the system. The CONTRACTOR role is the assignment-scoped role.

---

## Loading & Error States

| State | Behavior |
|-------|----------|
| Initial load | StatCard `loading={true}` (skeleton). Activity feed: 6 skeleton rows. Sidebar: 3 skeleton rows. |
| KPI fetch error | Individual StatCard shows error icon + "—" value. Other cards unaffected. |
| Activity feed fetch error | Feed area shows inline error message: "Could not load activity." with retry link. |
| Dashboard API total failure | Toast notification via notification system. Sections that failed show their individual error states. |
| Checklist dismiss failure | Toast error. Checklist re-appears (rollback optimistic update). Retry on next dismiss attempt. |

No full-page loading spinner. No full-page error boundary (app shell handles that layer).

---

## Frontend Implementation Notes

**Route:** `zync-app/src/routes/index.tsx` (or `_index.tsx` in Remix file routing). This is the root protected route rendered inside `<Shell>`.

**Data fetching:** Single `useQuery(['dashboard'], fetchDashboard)` with React Query. All sections read from the same cached response — no per-section queries.

**Module awareness:** `modules` object from the API response drives conditional rendering. Do not derive module state from KPI nulls — use the explicit `modules` map.

**Quick Actions:** Each button calls its own mutation (create invoice, create task, etc.) and on success invalidates the `['dashboard']` query key so KPIs and feed refresh.

**Responsive behavior:**
- < 768px (mobile): KPI row scrolls horizontally. Two-column layout stacks to single column (activity feed above, upcoming sidebar below). Quick Actions bar scrolls horizontally.
- 768px–1024px: Two-column layout maintained but sidebar narrower.
- > 1024px: Full 12-column layout as wireframe.

**Timezone:** All "today" and "tomorrow" computations on the server use `user_preferences.timezone` (IANA tz string, NOT NULL, e.g. `"Asia/Jerusalem"`). Client displays times in the same timezone.

---

## Architecture Decisions

| Decision | Choice | Rationale |
|----------|--------|-----------|
| Single API endpoint vs per-widget | Single `GET /api/dashboard` | Eliminates waterfall loading. Dashboard is always fetched as a unit. Simpler cache invalidation. |
| Server-side module filtering | `modules` map in response + server filters all data | Client never fetches data for disabled modules. Prevents phantom data leaking through UI. |
| Activity feed source | `audit_log` table (from `audit-compliance` spec) | No new events table. Audit log already captures the required event types with actor, entity, timestamp. |
| Checklist storage | Two columns on `tenants` (`checklist_dismissed_at`, `onboarding_completed`) | No per-user or per-role variants needed. Two columns on the existing tenants row avoids a join on every dashboard load and keeps the schema minimal. |
| Onboarding completion | Server-side, event-driven | `onboarding_completed` updated asynchronously on relevant mutations (not polled on dashboard load). Prevents stale UI for multi-user tenants where another admin completed a step. |
| Skeleton vs spinner | Skeleton loading on StatCard and feed rows | Reduces perceived load time and layout shift. Full-page spinner would block all content unnecessarily. |
| React Query staleTime | 60 seconds | Dashboard data does not need real-time accuracy. 60s avoids redundant refetches on tab focus while keeping data reasonably fresh. |
| Calendar events scoping | Per-user only | Calendar events are personal. Showing other users' events on the home screen is a privacy concern and adds noise. |
| Activity feed role filtering | Permission-based filter on audit_log entity_type query | No "STAFF" role exists. CONTRACTOR is the assignment-scoped role. All filtering via the standard permission system — no special cases. |
| audit_log entity_name joins | JOIN to entity tables on LIMIT 30 result | audit_log does not store entity_name. Joins are primary-key lookups on a small result set; acceptable. Denormalization is a future optimization if p95 degrades. |
| Module availability source | Derived from `tenant_modules` table (spec 32) | Each tenant independently enables/disables modules. The `modules` map in the API response is computed by reading the tenant's `tenant_modules` rows. Tier gating is orthogonal. |
| Open/overdue invoice status set | SENT, APPROVED, TAX_ISSUED | TAX_ISSUED (חשבונית מס issued) is an outstanding receivable awaiting payment — excluding it would undercount both KPIs. |
| Pending tasks definition | status_name IN ('TODO', 'IN_PROGRESS') | Brief requirements specify these two statuses explicitly. Non-terminal is broader (includes BACKLOG, BLOCKED, etc.) and would overcount. Custom status names may not match; implementer should treat these as the two canonical "in-progress" default statuses. |
