# CP Staff — Connelly Partners intranet

Internal site for `staff.connellypartners.com`. Next.js 15 (App Router) · Auth.js with Google Workspace sign-in · Postgres via Drizzle ORM · Tailwind v4.

Sections: **People** (profiles, skills, Workamajig project history), **Find a team** (rank best-fit staff for a pitch or project), **Case studies**, **Playbook** (services + how we deliver), **Showcase** (experiments, games, ideas), **Education** (Drive libraries by specialty), **Handbook**, **Office guides** (Dublin, Boston), **History** (timeline), plus a profile editor (`/me`) and an admin page (`/admin`).

---

## 1. Run it locally (10 minutes)

```bash
npm install
cp .env.example .env            # then fill in the values below
docker compose up -d db         # local Postgres (or point DATABASE_URL at your own)
npm run db:push                 # create tables
npm run db:seed                 # sample people, projects, case studies, etc.
npm run dev                     # http://localhost:3000
```

Sign-in is Google only, so before you can get past `/login` you need OAuth credentials (step 2). If you just want to see the pages, temporarily insert a session row — see "Dev sign-in" at the bottom.

## 2. Google Workspace sign-in

1. Google Cloud Console → **APIs & Services → Credentials → Create credentials → OAuth client ID** (type: Web application).
2. Authorised JavaScript origin: `https://staff.connellypartners.com` (and `http://localhost:3000` for dev).
3. Authorised redirect URI: `https://staff.connellypartners.com/api/auth/callback/google` (and the localhost equivalent).
4. On the OAuth consent screen choose **Internal** – this alone restricts sign-in to your Workspace.
5. Put the client ID/secret in `.env` as `AUTH_GOOGLE_ID` / `AUTH_GOOGLE_SECRET`, and generate `AUTH_SECRET` with `openssl rand -base64 32`.

`ALLOWED_GOOGLE_DOMAIN=connellypartners.com` is enforced server-side in `src/auth.ts` using Google's `hd` claim, so even a mis-configured consent screen can't let outsiders in. Emails listed in `ADMIN_EMAILS` get the **ADMIN** role on first sign-in; everyone else is **STAFF**. Roles can be changed at `/admin`.

On first sign-in a user is linked to their `person` row by email (created by the Workamajig sync) or, if none exists, a stub profile is created so they appear in the directory straight away.

## 3. Workamajig integration

Everything project-related (clients, projects, who worked on what and for how many hours) is mirrored from Workamajig into Postgres by `src/lib/workamajig/sync.ts`. The sync is idempotent and never overwrites fields staff edit themselves (bio, photo, interests, skills).

| Setting | Meaning |
|---|---|
| `WORKAMAJIG_MODE=mock` | Uses the bundled sample data in `src/lib/workamajig/mock.ts`. Default. |
| `WORKAMAJIG_MODE=api` | Live. Needs `WORKAMAJIG_HOST` (e.g. `https://app6.workamajig.com`), `WORKAMAJIG_ACCESS_TOKEN`, `WORKAMAJIG_USER_TOKEN`. |

Getting tokens: in Workamajig, **Admin → System Setup → Integrations/API** creates an *API Access Token* for the account and a *User Token* for a dedicated API user (give it read access to Users, Companies, Projects and Timesheets). The client sends them as the `APIAccessToken` and `UserToken` headers, hitting `https://<host>/api/<version>/{users|companies|projects|timesheets}`.

Field mapping lives in `src/lib/workamajig/api.ts` and is deliberately defensive — after your first real pull, run `npm run wmj:sync` and adjust the mappers if any column names differ in your account (Workamajig report fields vary by configuration).

Triggering a sync:

- `npm run wmj:sync` from a shell
- **Sync now** button on `/admin` (ADMIN role)
- `POST /api/workamajig/sync` with `Authorization: Bearer $SYNC_SECRET` — `vercel.json` schedules this nightly at 03:00 UTC; use any cron if you're not on Vercel.

## 4. How "Find a team" ranks people

`src/lib/match.ts`. The brief is tokenised, expanded with domain synonyms (film → video/motion/studio; hospitality → travel/hotel …), then every active person is scored on:

1. **Skills** (strongest; weighted by 1–5 proficiency)
2. **Role and department** title
3. **Project history** from Workamajig — matching project names, services, roles and client names, weighted by hours logged and recency, with a bonus for **client vertical** experience (Health, Education, Travel & Tourism …)
4. **Interests** (light touch)
5. Optional **capacity** nudge (fewer live projects → small boost)

Each hit is recorded as a human-readable reason so the UI can explain the ranking. `suggestTeam()` picks one person per discipline first, then fills remaining seats by score. The same engine is behind `/people?q=` and the JSON endpoint `POST /api/match` (for a Slack bot later). Tune weights and synonyms in that one file.

## 5. Content — where to edit what

| Content | Lives in | Edit how |
|---|---|---|
| Handbook chapters | `content/handbook/*.md` | Markdown with `title`/`summary` frontmatter; filename prefix sets order |
| Playbook services | `content/playbook.ts` | Typed objects: phases, outputs, team, tools |
| Office guides | `content/offices.ts` | Sections, nearby spots, lead, Wi-Fi note |
| Education specialties | `content/education.ts` + `education_resource` table | Add Drive folder / file links per specialty (`kind`: library, ebook, video, course, article, podcast) |
| Case studies | `case_study` table | Link `project_id` to a Workamajig project and the team credits appear automatically |
| Showcase | `/showcase/new` | Any signed-in staff member can post |
| History timeline | `history_event` table + `public/media/history/` | Drop `YYYY.jpg` / video files where the placeholders say |
| Profiles | `/me` | Staff edit their own photo, LinkedIn, bio, interests, skills |

A quick way to edit tables without building admin forms: `npm run db:studio` (Drizzle Studio). The natural next step is a small editor UI for case studies, education resources and history events, gated to the **EDITOR** role — the role and `requireRole()` helper are already in place.

## 6. Deploy to staff.connellypartners.com

Works anywhere Node runs. Easiest path is **Vercel + Neon/Supabase Postgres**:

1. Create a Postgres database; set `DATABASE_URL`.
2. Import the repo into Vercel; add every variable from `.env.example` (production values, `AUTH_URL=https://staff.connellypartners.com`).
3. Run `npm run db:push` once against the production database (or `npm run db:generate` + `db:migrate` for migration files), then `npm run db:seed` only if you want the sample content.
4. Add the `staff` CNAME for `connellypartners.com` to Vercel; the nightly Workamajig cron in `vercel.json` activates automatically.
5. Set `WORKAMAJIG_MODE=api` when tokens are ready and hit **Sync now** on `/admin`.

For a self-hosted box: `npm run build && npm start` behind nginx/Caddy with TLS, plus a system cron calling the sync endpoint.

## Design notes

Brand cues are lifted from connellypartners.com: Helvetica Neue, black/paper, the CP red highlight block (`.mark`), Instrument Serif italics for the "soul" moments, and the "we move customers' feet by first moving their souls" line. Tokens are in `src/app/globals.css` (`@theme`).

## Dev sign-in (no Google needed)

```sql
INSERT INTO "user" (id, name, email, role) VALUES ('dev', 'Dev User', 'you@connellypartners.com', 'ADMIN');
INSERT INTO "session" ("sessionToken", "userId", expires) VALUES ('devtoken', 'dev', now() + interval '7 days');
UPDATE person SET user_id = 'dev' WHERE email = 'cian.oreilly@connellypartners.com';  -- optional: attach a sample profile
```

Then set a cookie `authjs.session-token=devtoken` for `localhost` in your browser. Delete these rows before going live.

## Scripts

`dev` · `build` · `start` · `typecheck` · `db:push` · `db:generate` · `db:migrate` · `db:seed` · `db:studio` · `wmj:sync`
