# Team

**Status:** in production
**Lives at:** `/team` · `app/(app)/team/page.tsx`, `app/(app)/team/actions.ts`

## Summary
A reference table of the developers Adi manages, plus Adi himself. Each
member has a **cost rate** (USD per source hour Rian pays them) and
optionally a **billout rate** (USD per source hour Rian charges the
client). `consolidate_as` is the display name that surfaces on
summaries when entries should be grouped under a single label (e.g.
all of Adi's devs → "Tingang"). `rate_proportion` is now a legacy
field used only for the Toggl-export "converted hours" column.

## Why
2026-05-20 — "Adi maintains a list of his employees and their
percentage of his rate."
2026-05-25 — "this proportion of hours thing is causing complication
… we need to eliminate the 'proportion' and just have a rate. So
rather than a dev going through a filter of cutting his hours by
50% and then billing out at $14 should just move to his rate being
$7 and billing for all hours." → Migration in decisions.md #034
introduced `cost_rate_usd` as the canonical per-source-hour cost
and rescaled `billout_rate_usd` to also be per source hour.

## Behavior
- `/team` shows every team member in every team the user can see
  (Adi sees his team; Rian sees both stacked), sorted by `is_active`
  desc then `display_name`.
- Inline "Add member" form at the top of each team panel.
- Edit mode swaps a row for an inline form. Fields:
  - **Email**, **Display name**, **Consolidate as**, **Notes**,
    **Active** toggle.
  - **Cost rate** (USD/source-hour) — per-source-hour cost. Used
    by import + manual-entry to stamp `billout_cost_usd`.
  - **Billout rate** (USD/source-hour, owner-only) — what Rian
    charges the client. Used by import + manual-entry to stamp
    `billout_amount_usd`, before any project-level override.
  - **Proportion** — kept as input but labeled "Toggl export only".
    Source hours × proportion = exported hours on the Toggl CSV.
    Does NOT affect cost or billout anymore (post-2026-05-25).
- The "Base hourly rate" team-level panel is gone (it was
  `teams.base_rate_usd × proportion`, which is no longer the cost
  formula). The DB column is retained but unused — see decisions.md
  #034.
- `consolidate_as`:
  - For developers, set to `Tingang` — their entries appear as
    "Tingang" in `converted_user`.
  - For Adi himself, left blank — his entries use his `display_name`.
- Delete is permanent. Historical `time_entries` referencing a
  deleted team member get their `team_member_id` set to NULL (FK is
  `ON DELETE SET NULL`); already-stamped cost / billout stay as
  they were.

## Constraints & edge cases
- Email is unique per org (`unique (org_id, email)`).
- Emails are stored lowercase. The team-member lookup on imports is
  case-insensitive.
- Deleting a team member doesn't recompute anything on their historical
  entries — the conversion was applied at import time. If the
  proportion changed since, you'd need to re-import or use the entry's
  "Re-resolve from team" button to recompute with the current value.
- Adi's row must exist with email `info@adipramono.com` for the Summary
  page to find his hourly rate. If not present, Summary falls back to
  `$14/hr`.
- The backfill script's `derive_name(email)` is rough — it strips
  trailing digits and title-cases the local part. Names that came in
  as "Bayu" or "Hermonkarisma" are best fixed manually on the Team page.

## Permissions
- Managers and Owners can manage team. Super admins (not in view-as)
  also can. Members can't.

## Open considerations
- No "Re-resolve all entries from current team proportions" button.
  If Adi changes a proportion, only future imports use the new value.
  Existing entries can be edited individually via Entries → Re-resolve.
- No invite flow — team members aren't real users. They have no login.
- No way to bulk-edit or import the team list. CRUD only.
- `derive_name` is a one-shot heuristic; once a name's been edited,
  re-running the backfill would re-derive (would overwrite). Currently
  the backfill is one-shot anyway.

## Tests
- (Pure-logic for the conversion math is covered under CLK-018 …
   CLK-020 — see clockify tests.)
- 🟡 TEAM-001 Creating a duplicate email on the same org returns a 23505
  uniqueness error.
- 🟡 TEAM-002 Editing `rate_proportion` does NOT retroactively change
  historical entries (only future imports + explicit Re-resolve).

## Changelog
- **2026-05-20** — Initial CRUD page + create/update/delete actions.
- **2026-05-21** — Backfill from Rate Lookup tab seeded 18 members.
- **2026-05-21** — Removed per-member `hourly_rate_usd` input. Added a
  "Base hourly rate" panel at the top that edits
  `organizations.default_hourly_rate_usd`. Each member's effective rate
  is shown as a read-only derived value (`base × proportion`). See
  decisions.md #012.
- **2026-05-23** — **Teams per user.** Replaced the single org-wide
  team with a `teams` table, one team per `(org, user)`. Each team
  has its own `base_rate_usd`. `team_members.team_id` is now required.
  Email uniqueness is per-team (`unique (team_id, email)`), so
  `rian@rian.ca` can exist in both Adi's team and Rian's team. The
  /team page now renders one panel per visible team — Adi sees only
  his team; Rian sees both stacked. The import action scopes the
  email→member lookup to the importer's team and uses that team's
  rate for cost. See decisions.md #032.
- **2026-05-24** — Per-member **billout rate** added
  (`team_members.billout_rate_usd`, owner-only). Stamped onto
  `time_entries.billout_amount_usd` on import / re-resolve.
  See decisions.md #039.
- **2026-05-25** — **Eliminate rate_proportion** from the cost +
  billout pipeline. Each member now has a direct per-source-hour
  `cost_rate_usd`. Existing `billout_rate_usd` values were scaled by
  proportion so the migration was dollar-neutral on stamped entries.
  `rate_proportion` retained as input but only used by the Toggl
  CSV export. /team UI removed the "Base hourly rate" team-level
  panel and the derived "Effective rate" cell — both vestigial.
  See decisions.md #034.
