# SourceOpinions — Client Web Portal

An authenticated client workspace for the SourceOpinions managed customer-research
service, implemented per **`SourceOpinions_Client_Web_Portal_Specification_v1.1`** and
the house **`design_overview.md`** (procedural PHP + jQuery, no framework/build step).

It turns a managed research engagement into a transparent client workspace: submit a
research request, approve scope/profile/plan/materials, monitor recruitment and
fieldwork, review published findings, download deliverables, and manage stakeholders,
billing, files, calendar, and messages — without exposing participant identity or
internal analyst work.

## Quick start

The schema + a full demo are already installed in the `source_opinions` database.
To reinstall / reseed:

```bash
php setup.php fresh      # drop this app's tables, recreate schema, seed the demo
php setup.php            # create-if-missing + seed only when empty
```

Sign in (seeded demo — **Northwind Outdoors**):

| Email | Role | Password |
|-------|------|----------|
| dana@northwindoutdoors.com | Organization Owner | `demo12345` |
| aaron@northwindoutdoors.com | Administrator | `demo12345` |
| marcus@northwindoutdoors.com | Project Lead | `demo12345` |
| priya.shah@northwindoutdoors.com | Contributor | `demo12345` |
| jordan@northwindoutdoors.com | Viewer / Observer | `demo12345` |
| bill@northwindoutdoors.com | Billing Administrator | `demo12345` |
| sam@brightagency.co | External Guest | `demo12345` |

Seeded engagements: **SO-2041** (delivered pricing study — rich findings, deliverables,
debrief), **SO-2052** (active fieldwork), **SO-2068** (proposal awaiting approval), plus
draft/submitted research requests.

## Architecture (binding house style)

```
Browser GET dashboard.php
  → dashboard.php  (route file: loads the fixed config/auth chain, sets $thecontentis)
  → template.php   (master shell: head/topbar/sidemenu, includes content/$thecontentis)
  → content/dashboard.php  (HTML + inline jQuery; paints skeleton)
  → SO.ajax('samsdep/get_dashboard_data.php', …)  (separate request)
  → samsdep/get_dashboard_data.php  (re-bootstraps chain, validates auth, echoes JSON)
```

| Path | Responsibility |
|------|----------------|
| `*.php` (root) | One thin route file per page. Loads the config chain → sets `$thecontentis` → `include "template.php"`. No markup, no business queries, no JSON. |
| `config/` | `session`, `master_config`, `dba` (MySQLi wrapper), `helpers` (all `so_*`), `sdk`, `auth` (page gate), `auth2` (endpoint gate), `project_auth` (project-access gate), `branding`. |
| `inc/` | `ui.php` (icon/avatar atoms), `files.php` (presigned upload/download helpers), `seed.php` (demo data). |
| `content/` | Page bodies — HTML + inline jQuery. Never render `<html>`/global nav. |
| `samsdep/` | One procedural JSON endpoint per operation. Re-bootstraps the chain, `auth2`, `so_require_csrf()` for mutations, one op, `so_ok()`/`so_err()`. |
| `cron/` | CLI queue/reconciliation scripts (`send_email_queue`, `send_notification_queue`, `sync_operations`, `process_file_scans`, `reconcile_payments`, `expire_tokens`). |
| `css/ js/ img/ fonts/` | Local static assets — no npm/build. `css/theme.css` + `js/app.js` (`window.SO`). |
| `storage/ logs/` | Simulated object storage + protected logs (web-writable). |

### Key conventions
- **DB:** only the `dba` MySQLi wrapper, string-interpolated SQL, `$dba->clean()` on
  every user value; sort/status/columns from server-side allowlists. No ORM/PDO.
- **Auth:** PHP session + a persisted **keycode** validated on every page (`auth.php`)
  and endpoint (`auth2.php`). Identity/org/role come only from the session.
- **Tenant isolation:** every query is scoped to the session organization + the user's
  membership/project access (`so_project_access`). Deny-by-default; possession of an
  id is never authorization.
- **Envelope:** `{ "status": "success" | "error", "msg", … }`; CSRF token on mutations.
- **Files:** presigned upload (`get_upload_policy` → browser POST → register endpoint)
  and short-lived signed downloads (`get_download_url` → `download.php`). PHP is out of
  the byte path in live mode.
- **No secrets in code:** DB/provider credentials come from the environment
  (`master_config` reads `getenv()` with dev fallbacks). Cloud SDK clients (when live)
  use the instance IAM role.

### Provider mode
`master_config['provider_mode']` defaults to `simulate`: email/payment/e-sign/calendar
and object storage are modelled locally so the whole portal runs end-to-end with no
live credentials. Set `PROVIDER_MODE=live` (and supply the SDK + env credentials) to
swap in real providers at the same call sites.

## Roles (user_type)
`100` Owner · `101` Administrator · `102` Project Lead · `103` Contributor ·
`104` Viewer/Observer · `105` Billing Administrator · `106` External Guest ·
`1` SourceOpinions Team (internal; publishes content via the separate ops console).
Project-level approval capabilities (scope/profile/plan/materials/deliverable) are
assignment-based, stored per member in `project_members`.
