# TAPLY — Implementation Plan

> **Source of truth:** `plan/SRS.md`. This document is the *how*; the SRS is the
> *what*. If they disagree, the SRS wins.
>
> **Supersedes:** `plan/MVP.md` (an earlier sketch written before the SRS existed).
>
> **Frontend is frozen** at the current Blade views (`resources/views/card/*.blade.php`).
> No layout, CSS, or component-level redesign in this phase. All work is:
> 1. Backend (routes, controllers, models, migrations, auth, file storage)
> 2. **New** admin/company views generated from the Tyro Dashboard scaffold —
>    these aren't a redesign because they don't exist yet.

---

## 1. Scope (what we will and won't build)

### In scope for MVP (mapped to SRS sections)

| SRS § | Capability |
|---|---|
| §2.1 | Four user types: admin, company, employee, independent |
| §3.1 | Admin: CRUD companies, view/activate/deactivate any profile |
| §3.2 | Company: CRUD employees under their company |
| §3.3 | Independent: self-serve signup, edit own profile |
| §3.4 | Profile creation auto-generates user account, slug, QR, NFC URL |
| §3.5 | Visitor lands on public profile → "Edit profile" requires auth |
| §3.6 | Edit name, role, company info, phones, emails, socials, images |
| §3.7 | Preview before publish (keeps current sessionStorage flow) |
| §3.8 | Publish / Update writes to DB, public view reflects |
| §3.9 | Save Contact downloads vCard 3.0 (.vcf) |
| §3.10 | QR code generated per profile, links to public URL |
| §3.11 | NFC card stores public URL (operations process, see §11 below) |
| §3.12 | Call / Email / WhatsApp / Website / Save Contact / Social buttons all work |
| §4.2 | Session auth (Laravel default), bcrypt passwords, role-based middleware |
| §4.5 | Mobile-first UI — already true in the locked frontend |

### Deferred (SRS §6 — post-MVP)

- §6.1 Analytics dashboard (views / scans / taps / downloads)
- §6.2 Lead capture popup
- §6.3 OCR business-card scanner
- §6.4 Multi-theme — partially shipped already via cover-as-theme; full theme
  customization deferred
- §6.5 Custom domain support (`name.yourapp.com`)
- §6.6 Offline mode
- §6.7 Team management beyond basic employee CRUD
- §6.8 CRM integrations
- §6.9 Verified badge
- §6.10 Subscription billing
- §3.6 "Custom fields" — out of MVP; not worth the schema complexity yet

### Out of scope, period (not even later)

- JWT (SRS §4.2 offers it as alternative — we pick **session** because the
  frontend is server-rendered Blade; JWT only matters for SPA/mobile)
- Native mobile apps (SRS §8 — far future)

---

## 2. Decisions made up front

These were ambiguous in the SRS or open from the prior review. Locking them now:

| Decision | Choice | Why |
|---|---|---|
| Auth | Laravel session auth, bcrypt passwords | Frontend is Blade; JWT adds complexity with no payoff |
| Login UI | Reuse `card/login.blade.php`; sign-up gets a sibling `card/register.blade.php` matching the same visual style | Frontend is frozen but a new sibling view is consistent, not a redesign |
| Tyro Dashboard | Used **only** for `/admin` and `/company` portals | The card-owner flow keeps the current bespoke design |
| One profile per user | `profiles.user_id` UNIQUE | SRS implies it; simpler permissions |
| Slug strategy | Auto from full_name on create, owner can edit later, collision = numeric suffix | UX tradeoff: easy default + power user override |
| Reserved slugs | `admin`, `company`, `api`, `card`, `u`, `login`, `register`, `logout`, `storage` blocked at validation | Avoid route collisions |
| File storage | `storage/app/public/{photos,logos}` via `storage:link` | Fine for MVP; S3 swap is a single config later |
| Image upload | Keep client cropper, POST data URL, server decodes → JPEG/PNG file | Reuses the existing edit-page UX |
| vCard version | **3.0** | Maximum iOS compatibility |
| QR library | `simplesoftwareio/simple-qrcode` (or pure SVG via `bacon/bacon-qr-code`) | Mature, no JS dep |
| Email delivery | `MAIL_MAILER=log` for MVP, real SMTP in deploy | Don't block dev on SMTP config |
| Audit logging | Spatie's `activitylog` package — minimal events only | Cheap insurance for admin actions |
| Signup policy | **Open** — anyone can register at `/card/register` | B2C growth path; reserved-slug list + admin deactivate covers abuse |
| Per-company branding (Tyro portal) | **Deferred** — uniform Tyro look in Phase 5 | Keeps Phase 5 small; revisit when a real B2B customer asks |
| Production hosting | **Decide in Phase 7** | MVP code doesn't depend on it; premature lock-in risks wrong constraints |

---

## 3. Data model

```
companies
  id, name, logo_path, is_active, timestamps
  └── hasMany users (employees)

users
  id, name, email (unique), password, role (enum: admin|company|employee|independent),
  company_id (FK companies.id, NULL for admin/independent)
  is_active, timestamps
  └── hasOne profile

profiles
  id, user_id (unique FK)
  slug (unique, indexed)            ← /u/{slug}
  full_name, role_title, company_display_name, bio
  profile_photo_path, company_logo_path
  cover_name  (enum: Teal|Violet|Indigo|Sunset|Emerald|Fuchsia|Midnight)
  layout      (enum: centered|left|magazine)
  appearance  (enum: dark|light)
  whatsapp, website, address
  is_published (bool, default false)
  published_at (nullable timestamp)
  timestamps

profile_phones    (id, profile_id, label[Mobile|Work|Home], number, position)
profile_emails    (id, profile_id, label[Personal|Work], address, position)
profile_socials   (id, profile_id, platform[enum 13 values], url, position)

# Deferred to Phase 6+ (created as empty tables / not at all in MVP):
# profile_views, scan_events, contact_downloads, leads
```

Notes:
- **`company_display_name`** on the profile is intentional: an employee's
  shown company can differ from their formal `companies.name` (e.g. brand
  vs legal name). For independents it's just whatever they type.
- **`profile_links` from the SRS is split** into `profile_phones`, `profile_emails`,
  `profile_socials` — one table can't carry three different shapes cleanly.
- **`qr_codes` from the SRS is dropped** — QR is derivable from `slug`, no
  state to store. (When analytics arrives, a `scan_events` table tracks usage.)
- **All enums** are PHP 8 backed enums under `app/Enums/` — single source of
  truth for DB, validation, Blade.

---

## 4. Role × capability matrix

|   | Admin | Company | Employee | Independent | Visitor |
|---|:---:|:---:|:---:|:---:|:---:|
| View any published profile | ✅ | ✅ | ✅ | ✅ | ✅ |
| Save vCard from a profile | ✅ | ✅ | ✅ | ✅ | ✅ |
| Manage all companies | ✅ |  |  |  |  |
| Manage all profiles (activate/deactivate) | ✅ |  |  |  |  |
| View platform metrics | ✅ |  |  |  |  |
| Manage employees under own company |  | ✅ |  |  |  |
| View own company's employee profiles |  | ✅ |  |  |  |
| Edit / publish own profile |  |  | ✅ | ✅ |  |
| Self-serve sign-up |  |  |  | ✅ |  |

Enforced via Laravel **policies** (`CompanyPolicy`, `ProfilePolicy`) and a
`role` middleware (`Route::middleware('role:admin')`).

---

## 5. Route map

```
PUBLIC (no auth)
  GET  /                     → marketing/landing (deferred; show /card for now)
  GET  /u/{slug}             → PublicProfileController@show
  GET  /u/{slug}.vcf         → PublicProfileController@vcard       (SRS §3.9)
  GET  /u/{slug}/qr.png      → PublicProfileController@qr          (SRS §3.10)

AUTH (Laravel session)
  GET  /card/login           → existing view
  POST /card/login           → AuthController@login
  POST /card/logout          → AuthController@logout
  GET  /card/register        → new view for independents             (SRS §3.3)
  POST /card/register        → AuthController@register

OWNER (auth required, role: employee|independent|company)
  GET  /card                 → MyProfileController@show              (own profile preview-ish)
  GET  /card/edit            → MyProfileController@edit
  POST /card                 → MyProfileController@update            (SRS §3.6, §3.8)
  GET  /card/preview         → MyProfileController@preview           (SRS §3.7)
  POST /card/photo           → MyProfileController@uploadPhoto       (SRS §3.6)
  POST /card/logo            → MyProfileController@uploadLogo

COMPANY (auth + role:company)
  Tyro Dashboard scaffold under /company/*
  - Employee CRUD
  - View employee profiles

ADMIN (auth + role:admin)
  Tyro Dashboard scaffold under /admin/*
  - Company CRUD (SRS §3.1.1)
  - Profile CRUD + activate/deactivate (SRS §3.1.2)
  - User CRUD
```

---

## 6. Phases

Each phase ships independently. Don't start phase N+1 until N is green.

### Phase 1 — Foundation: auth + schema
*No visible UI change. Existing pages still show hardcoded content.*

- Add `role`, `company_id`, `is_active` to `users` (migration).
- Create `companies`, `profiles`, `profile_phones`, `profile_emails`, `profile_socials`.
- PHP enums under `app/Enums/`: `UserRole`, `CoverName`, `Layout`, `Appearance`,
  `PhoneLabel`, `EmailLabel`, `SocialPlatform`.
- Eloquent models with relations; cast enum columns to enum classes.
- `RoleMiddleware` registered, named `role`.
- `AuthController` (login / logout / register).
- Seed the existing demo data (Md. Jahidul Islam Riyad + WAC Bangladesh) as
  one independent user + their profile + phones/emails/socials. The demo
  account is the canary for every later phase.

**Done when:** `php artisan migrate:fresh --seed` produces the demo user; signing
in at `/card/login` lands on `/card/edit` (still rendering the same static look);
logout returns to `/card/login`.

---

### Phase 2 — Public read path
*The public card becomes real data.*

- `PublicProfileController@show($slug)` — resolves profile, 404 if missing
  or `is_published=false`, "Card not yet published" notice if owner viewing
  their own draft.
- Refactor `card/index.blade.php` to render from `$profile` (loops over phones,
  emails, socials).
- Inject server-side state to the layout:
  `<script>window.TAPLY_CARD = @json($profile->toCardConfig())</script>` so
  `TaplyState.apply()` picks it up — replacing `TaplyState.loadSaved()` for
  the public route.
- Slug generation hook on `Profile::creating`.

**Done when:** `/u/jahidul-riyad` renders the demo profile pixel-identically to
today. Mutating the DB changes what's shown after a refresh.

---

### Phase 3 — Owner write path
*Edit form actually saves.*

- `MyProfileController@edit` returns the existing `card/edit.blade.php`,
  pre-populated from `auth()->user()->profile`.
- `UpdateProfileRequest` (FormRequest) validates: name lengths, URL formats,
  enum values, max 10 phones, max 10 emails, max 15 socials.
- `MyProfileController@update` syncs the four tables (`profiles`,
  `profile_phones`, `profile_emails`, `profile_socials`) — delete-missing,
  upsert-rest, keyed by `position`.
- Replace the inline `window.location.href = preview` JS with a fetch POST
  to `/card`. On 200, navigate to preview; on 422, surface validation errors.
- **Image upload pipeline:**
  - Cropper continues to produce a JPEG data URL.
  - On Save, POST as `profile_photo_b64` / `company_logo_b64` fields.
  - Server: validate base64 → MIME via `getimagesize()` → store under
    `storage/app/public/photos/{uuid}.jpg` (or `.png`) → save relative path.
  - Old file deleted on replace; nullable on remove.
- The Preview flow keeps using sessionStorage — owner-only, no round-trip needed.

**Done when:** logging in as the demo owner, editing any field, clicking
SAVE CHANGES, then refreshing `/u/jahidul-riyad` shows the new value. A
newly cropped photo survives logout/login.

---

### Phase 4 — Admin panel (Tyro Dashboard)
*Implements SRS §3.1.*

- Publish Tyro Dashboard assets/config if not done; configure brand/colors.
- `/admin` route group, `auth + role:admin` middleware, Tyro layout.
- Resources / "modules":
  - **Companies** — list, search, create, edit, activate/deactivate. Creating
    a company auto-creates a `company`-role user with a generated password
    (shown once to admin, emailed if SMTP configured).
  - **Profiles** — list (search by slug/full_name/company), view, activate/
    deactivate, **cannot edit content** (owner's job). Admin can change `slug`
    in case of takedown.
  - **Users** — list, suspend (`is_active=false`), trigger password reset.
  - **Dashboard landing** — counts (companies, profiles, published) with
    Spatie activitylog feed of recent admin actions.

**Done when:** admin can sign in, create a company, that company's user can
sign in, see their (empty) employee list, log out cleanly.

---

### Phase 5 — Company portal
*Implements SRS §3.2.*

- `/company` route group, `auth + role:company` middleware, Tyro layout.
- **Employees** — list (scoped to `auth()->user()->company_id`), create,
  edit, suspend. Create flow: provide name + email → server generates User
  (role=employee, company_id=current) + Profile + password → password
  rendered once (and emailed if SMTP up).
- **Company profile (optional):** if you want a `/c/{slug}` company page,
  add it here. **Defer unless explicitly requested** — not in the SRS.

**Done when:** company user creates an employee; the employee receives
credentials (visible in `storage/logs/laravel.log` with `MAIL_MAILER=log`);
employee logs in and edits their card.

---

### Phase 6 — NFC ops, QR, vCard
*Closes out SRS §3.9–§3.11.*

- `PublicProfileController@vcard` — render vCard 3.0 from `$profile`. Returns
  `Content-Type: text/vcard; charset=utf-8` with `Content-Disposition:
  attachment; filename="{slug}.vcf"`. Test on iOS Safari and Android Chrome.
- `PublicProfileController@qr` — render QR code PNG (cached 24h) encoding
  the canonical profile URL. Throttle the route.
- Wire the Save Contact buttons on `card/index.blade.php` and `card/preview.blade.php`
  to point at `/u/{slug}.vcf`.
- Wire the QR placeholder (`<div class="qr-box">`) on the public card to
  display an `<img src="/u/{slug}/qr.png">`.
- Document the **NFC writing operations process** in `docs/nfc-ops.md`:
  use Android with NFC Tools to write `https://yourapp.com/u/{slug}` to
  NTAG215 cards; iOS Shortcuts as alternative. This is a fulfillment
  procedure, not code.

**Done when:** scanning the QR on a published profile downloads the right
vCard; a physical NFC card with the URL written, tapped to any modern
phone, opens the profile.

---

### Phase 7 — MVP polish
- Reserved-slug validator.
- Throttle public routes (`throttle:60,1`).
- Storage link in deploy notes (`php artisan storage:link`).
- "Profile not published" view (themed to match the card aesthetic).
- 404 view for unknown slugs.
- Owner-initiated profile deletion (data export = vCard + JSON dump).
- README with setup, env vars, seed instructions.
- Smoke-test checklist for every role-route combination.

**Done when:** the checklist passes on a fresh `migrate:fresh --seed` install.

---

## 7. Cross-cutting concerns

- **Validation:** every write route uses a FormRequest. No raw `$request->validate()`
  in controllers — keeps rules versionable per endpoint.
- **Authorization:** every model with role-based access has a Policy. No
  `if ($user->role === 'admin')` checks in controllers — use `Gate` / `authorize`.
- **Storage paths in DB** are relative (`photos/abc.jpg`), never absolute URLs.
  Render via `Storage::url(...)` so swapping disks is a config change.
- **Enums** are the single source of truth — no hardcoded `'dark'`, `'centered'`,
  `'Teal'` strings outside the enum class.
- **Audit:** log company create/deactivate, profile activate/deactivate,
  user suspend, password reset via Spatie activitylog. Skip noisy events
  (profile edits by owner).
- **Tests:** at minimum, one feature test per role × phase end-state. No
  unit-test religion; feature tests catch the right regressions for a Blade
  app.
- **Rate limiting:** `throttle` middleware on `/u/{slug}*` (public read),
  `/card/login`, `/card/register`. Defaults are fine for MVP.

---

## 8. Risks (and the cheapest mitigation)

| Risk | Mitigation |
|---|---|
| Image upload abuse (large files, bad MIME) | Cap at 2 MB pre-base64; validate MIME via `getimagesize`; store as JPEG re-encoded server-side (the cropper already does this client-side) |
| Slug squatting (someone grabs `ceo`, `apple`) | Reserved-slug list + admin can rename any profile |
| Visitor-side profile takedown abuse | Admin `is_active=false` flag flips public view to a "this profile is unavailable" page |
| Employee leaves company | Company user can deactivate employee (suspends the user, public profile shows "unavailable"). Reassigning ownership is post-MVP |
| Cropper data URLs blowing the request size | `post_max_size = 8M`; client-side 2 MB cap; cropper already JPEG-compresses |
| iOS Safari vCard quirks | Use vCard 3.0; smoke-test on real iPhone before shipping |
| Tyro Dashboard hard-codes assumptions we can't override | Tyro UI is **scoped to admin/company only**; the card-owner flow never depends on it |

---

## 9. What's *not* in this plan (and shouldn't be added without discussion)

- Anything visual in the four locked card views (`card/{index,edit,login,preview}.blade.php`).
  Pure read/write wiring only.
- A landing/marketing page at `/`. For now, `/` can redirect to the demo
  card or a `/card/login` link.
- Email templates beyond the bare credential-delivery message.
- i18n / Bangla support. The current UI strings stay English; add later
  if a customer needs it.

---

## 10. Open questions — resolved

All MVP-blocking ambiguities are now locked into §2. For the record:

1. **Signup policy** → **Open**. `/card/register` exists in Phase 1; reserved-slug
   list + admin `is_active=false` is our abuse mitigation.
2. **Per-company Tyro branding** → **Deferred**. Phase 5 ships a single look.
3. **Photo crop output** → kept as-is. Client cropper produces JPEG; server
   trusts the MIME after `getimagesize()` validation.
4. **Hosting target** → **Decide in Phase 7**, not now. Code stays portable
   (no S3-specific calls, no Forge-specific config) until then.

If any of these need revisiting, edit §2 (the decisions table) and update
the affected phase — don't reopen this section.

---

## 11. NFC operations process (non-code)

Not buildable in software — documenting so it's not forgotten:

1. Admin orders **NTAG215** blank stickers/cards (capacity > URL length).
2. Using **NFC Tools** (Android) or **Apple Shortcuts** (iOS 14+), write the
   profile's canonical URL: `https://taply.app/u/{slug}`.
3. Lock the tag (NFC Tools → "Lock tag") to prevent overwrite.
4. Ship to the user.
5. **Re-writes** require physical access — there's no "remotely re-program
   the card" capability on consumer NFC tags. Plan for this with customer
   support.

---

## Quick sequencing visual

```
Phase 1  ██████░░░░░░░░░░░░░░  schema + auth (no visible change)
Phase 2  ░░░░░░██░░░░░░░░░░░░  public profile is dynamic
Phase 3  ░░░░░░░░████████░░░░  edit form persists (biggest phase)
Phase 4  ░░░░░░░░░░░░░░░██░░░  admin panel
Phase 5  ░░░░░░░░░░░░░░░░██░░  company portal
Phase 6  ░░░░░░░░░░░░░░░░░██░  QR + vCard + NFC ops
Phase 7  ░░░░░░░░░░░░░░░░░░██  polish
```
