# Time Entries → Invoice Creation UI

**Date:** 2026-05-31  
**Status:** Draft  
**Spec:** 77  
**Tier:** All tiers  
**Depends on:** `time-management`, `invoices-core`, `projects-module`, `foundation-auth-rbac`  
**Referenced by:** `time-management`, `invoices-core`

---

## Overview

UI for selecting unbilled time entries and generating an invoice from them. Closes the loop between spec 13 (time tracking) and spec 15 (invoices). Time entries gain an `invoice_id` column tracking which invoice they were billed on (column name per migration order constraint in index).

---

## Data Model Change

```sql
ALTER TABLE time_entries ADD COLUMN invoice_id UUID REFERENCES invoices(id) ON DELETE SET NULL;
ALTER TABLE time_entries ADD COLUMN billed_at TIMESTAMPTZ;
```

Unbilled = `invoice_id IS NULL` and `billable = true`.

---

## Trigger Points

### 1. From the Invoice New Form

"New Invoice" (spec 15) gains a "Bill time entries" button:

```
┌────────────────────────────────────────────────────────────┐
│  New Invoice                                               │
│  Customer: [Acme Corp ▾]   Project: [Website Redesign ▾]  │
│                                                            │
│  Line items:                                               │
│  [Add line item]   [Bill time entries →]                   │
└────────────────────────────────────────────────────────────┘
```

Clicking "Bill time entries" opens the time selector (see below).

### 2. From `/time` page (spec 13)

The time log page gains a "Generate Invoice" button when unbilled entries are selected via checkboxes:

```
┌────────────────────────────────────────────────────────────┐
│  Time                                                      │
│  [2 entries selected]              [Generate Invoice →]    │
│                                                            │
│  ☑  2026-05-28  Website Redesign  Homepage design  2h 30m  │
│  ☑  2026-05-27  Website Redesign  API work         1h 45m  │
│  ☐  2026-05-26  Internal          Planning         0h 30m  │
└────────────────────────────────────────────────────────────┘
```

"Generate Invoice" → navigates to `/invoices/new?project_id={projectId}&from_time=true` with selected entry IDs in session/URL.

---

## Time Entry Selector Modal

Shown when "Bill time entries" is clicked in invoice creation. Auto-filters to the selected customer + project:

```
┌──────────────────────────────────────────────────────────────┐
│  Select time entries to bill                                 │
│  Customer: Acme Corp  ·  Project: Website Redesign           │
│                                                              │
│  [All unbilled ▾]  [Date range ▾]  [User ▾]                 │
│                                                              │
│  ☑  2026-05-28  Alex Katz   Homepage design    2h 30m  ₪875  │
│  ☑  2026-05-27  Alex Katz   API work           1h 45m  ₪613  │
│  ☑  2026-05-26  Dana Levi   Code review        1h 00m  ₪350  │
│  ☐  2026-05-20  Alex Katz   Planning call      0h 45m  ₪263  │
│                                                              │
│  Selected: 3 entries   Total: 5h 15m   Amount: ₪1,838       │
│                                                              │
│  Line item grouping:                                         │
│  ○ One line item per entry                                   │
│  ● One line item per day (grouped)                           │
│  ○ One line item total                                       │
│                                                              │
│  Hourly rate: ₪[350___]/hr  (from project settings)         │
│                                                              │
│  [Cancel]              [Add to Invoice →]                    │
└──────────────────────────────────────────────────────────────┘
```

Amount = hours × hourly rate (from project's `hourly_rate` field, editable here if needed).

"Add to Invoice" → closes modal, appends line items to the invoice form using the selected grouping. Entry IDs stored in `pendingBilledEntries[]` (committed when invoice is saved).

---

## Invoice Save: Mark Entries as Billed

When the invoice is saved (`POST /api/invoices`) with `billedEntryIds[]` in the body:

1. Creates the invoice record
2. Sets `time_entries.invoice_id = newInvoiceId, billed_at = now()` for all entries in `billedEntryIds`
3. Both in the same transaction

If the invoice is later deleted or voided: `invoice_id` is set back to NULL (via `ON DELETE SET NULL` FK) — entries become unbillable again.

---

## Unbilled Indicator

On the `/time` page (spec 13), unbilled billable entries show a subtle indicator:

- Dot badge: `⚬` (hollow) = unbilled billable; `●` = billed

On `/projects/:id` detail (spec 10), "Unbilled hours" chip:

```
[5h 30m unbilled — Generate Invoice]
```

Click → `/invoices/new?project_id={id}&from_time=true` pre-filtered to that project.

---

## API Extensions

```
GET  /api/time?unbilled=true&projectId=&customerId=
  → filter to unbilled billable entries (existing endpoint extended)

POST /api/invoices
  → extended: accepts billedEntryIds?: string[] in body
     On create: marks those entries as billed in same transaction
```

---

## Architecture Decisions

| Decision | Choice | Reason |
|----------|--------|--------|
| `invoice_id` on time_entries | Not a join table | One-to-one: each entry billed on at most one invoice; column is simpler |
| Grouping options | One per entry / per day / total | Different clients want different levels of detail on invoices; one-total hides hours detail some clients need |
| Hourly rate from project | Not per-entry | Project-level rate is the most common configuration; per-entry rate is a future extension |
| Mark billed in same transaction | Not async | Atomicity required: invoice must not be saved without entries being marked, and entries must not be marked without invoice existing |
