# Implementation Report — Audit & completion pass

## Deploy steps
1. `composer update` — adds `barryvdh/laravel-dompdf` (PDF generation). This is the only new dependency.
2. `php artisan migrate` — runs one new migration (proposal wizard fields, versions, templates and email tracking tables). It rolls back cleanly.
3. For real email delivery, set `MAIL_MAILER=smtp` and `MAIL_*` in `.env`. With the default `MAIL_MAILER=log`, emails are written to `storage/logs/laravel.log` and are not delivered.
4. Set `APP_URL` to the public URL. Tracking pixels and links point to it.

## How it was verified
- **Database:** a SQLite copy, rebuilt from the project's own migrations and seeder.
- **Pages:** every GET route was loaded as both Superadmin and BDE. All return 200 or the intended redirect.
- **Server-side test scripts:** 53 checks on CSV import/export and filters, plus 65 checks on the proposal flow:
  - create → commercials → custom-rate approval → field permissions → editor → preview
  - PDF output (`%PDF` header verified)
  - email send (pixel, signed links, PDF attachment), then open/click tracking
  - detail-page actions, versions (revise / compare / restore), template editing, BDE authorization
- **Real browser (Chromium):** the same flows were clicked through with real CSRF tokens. That covered UI import, export download, filters, the full wizard, PDF download, sending email, notes, share link, follow-up, the template Edit modal and the approval decision form.
- **Console:** no JavaScript console errors on any tested page, at desktop and mobile widths.
- **Not tested:** MySQL. No MySQL server was available here, so all checks ran on SQLite.

## Changed files

### Shared infrastructure
| File | Type | What / why |
|---|---|---|
| `app/Http/Controllers/Concerns/CsvTools.php` | **New** | One shared helper for streamed CSV export (UTF-8 BOM, formula-injection safe, respects the current query/filters), sample CSV download, and CSV import. Import validates headers and rows, handles duplicates (skip or update), runs in a transaction, and returns a row-level error report. New because 12 controllers needed it; duplicating it per controller would have been worse. |
| `app/Http/Controllers/Controller.php` | Modified | Uses the `CsvTools` trait. |
| `resources/views/partials/import-modal.blade.php` | **New** | Import modal reused by 6 pages: file input, duplicate mode, sample CSV link, error display. |
| `resources/views/layouts/app.blade.php` | Modified | Shows `error` flashes and import row errors (before, only `status` was shown). Adds a `csrf-token` meta tag. Adds the `proposal-page` body class on proposal screens: `proposal.css` scopes all its styles under it, and it was missing, so stepper, KPI cards, status pills, permission rows and the PDF preview all rendered unstyled. This was the main styling bug. |
| `app/Models/User.php` | Modified | `isAdmin()` (Superadmin / Admin / Manager), used for approvals, templates, permissions and proposal ownership checks. |
| `routes/web.php` | Modified | Export, import and sample routes for every list; routes for the clients, wizard, PDF, email, tracking, versions, templates and permissions controllers; public `/t/o`, `/t/c` and `/p` routes (throttled). `/proposals` now redirects to the real list. |

### Tables: search, filters, export, import
| File | Type | What / why |
|---|---|---|
| `app/Http/Controllers/CompanyController.php` | Modified | `filtered()` query shared by the list and the export; industry, location, type, status and owner filters; `export`, `import`, `importSample`. Import was a dead button before. |
| `resources/views/pages/companies.blade.php` | Modified | Toolbar is now a real GET form; selects are filled from the DB and auto-submit. Reset, status tabs (as links), Export CSV link and import modal now work. Filtered empty state added. |
| `app/Models/Company.php` | Modified | Added the missing `opportunities`, `requirements` and `proposals` relations. **Company Detail and Lead Detail crashed with a 500 error without them.** |
| `app/Http/Controllers/ContactController.php` + `pages/contacts.blade.php` | Modified | Company and decision-maker filters, export, import (new Import button), Reset. Stale "placeholder" text replaced. |
| `app/Http/Controllers/LeadController.php` + `pages/leads.blade.php` | Modified | Source, industry, owner and status filters; status tabs; export; import. The hard-coded "Converted (58)" tab count is now real. |
| `app/Http/Controllers/OpportunityController.php` + `pages/opportunities.blade.php` | Modified | Stage, industry, owner and expected-close filters; stage tabs; export; import. |
| `app/Http/Controllers/RequirementController.php` + `pages/requirements.blade.php` | Modified | Company, industry, BDE and status filters; tabs; export; import. The hard-coded "Cancelled (6)" count is now real. |
| `app/Http/Controllers/ActivityController.php` + `pages/activities.blade.php` | Modified | Type, related-to, user and status filters; Upcoming / Completed / Overdue tabs; export. |
| `app/Http/Controllers/FollowupController.php` + `pages/followups.blade.php` | Modified | Status, channel and owner filters; export. |
| `app/Http/Controllers/TaskController.php` + `pages/tasks.blade.php` | Modified | Status, priority and assignee filters; export. |
| `app/Http/Controllers/InvoiceController.php` + `pages/invoices.blade.php` | Modified | The company, status and date-range filters existed but did nothing; now wired. Export includes paid amount and balance. |
| `app/Http/Controllers/PaymentController.php` + `pages/payments.blade.php` | Modified | Search box added (the controller already supported it), mode filter, export. |
| `app/Http/Controllers/AuditController.php` + `pages/audit-log.blade.php` | Modified | Query moved to `filtered()`; export and Reset added. |
| `ServiceMasterController.php`, `CommercialMasterController.php` + their views | Modified | Export CSV. |
| `app/Http/Controllers/ClientController.php` | **New** | The Clients page was a static mock with fake numbers, frontend-only search and dead Import / Add / Export buttons. There was no controller, so one was needed. It provides CRUD, filters, real stats, export and import on the existing `clients` table. |
| `app/Models/Client.php` | Modified | `company` and `owner` relations. |
| `resources/views/pages/clients.blade.php` | Modified (body rebuilt) | Table, stats, toolbar and modal bound to real data. The header and stat-card markup were kept. |

### Proposal module
| File | Type | What / why |
|---|---|---|
| `database/migrations/2026_10_12_000001_extend_proposal_module.php` | **New** | Proposal columns needed by the wizard and PDF (client details, requirement, commercials, terms, `sent_at`, `share_token`), plus four new tables: `proposal_templates` (seeded with the 5 templates that were hard-coded cards), `proposal_versions`, `proposal_emails` and `proposal_email_events`. |
| `app/Models/Proposal.php` | Modified | Casts, status constants, relations, `recalculate()` (server-side totals), version snapshot sync on save, `editableBy()`, `statusClass()`, merge tags. |
| `app/Models/ProposalTemplate.php`, `ProposalVersion.php`, `ProposalEmail.php`, `ProposalEmailEvent.php` | **New** | Models for the new tables. Templates hold the editable / locked field-permission map. |
| `app/Http/Controllers/ProposalWizardController.php` | **New** | Create → Commercials → Editor → Preview steps and PDF. These pages were served by `PageController` with no backend at all. Enforces Commercial Master rates, custom-rate approval and template field permissions on the server. Opening a step without a proposal shows a picker instead of fake data. |
| `app/Http/Controllers/ProposalEmailController.php` | **New** | Compose / send (PDF attached, tracking added), Email Tracking page and export, public open-pixel, signed click redirect and the public share page / PDF. |
| `app/Http/Controllers/ProposalVersionController.php` | **New** | Version list, compare, view snapshot, revise (next version), restore (as a new version). |
| `app/Http/Controllers/ProposalTemplateController.php` | **New** | Template list, create and update (updates in place, so no duplicates; unique name check), field-permission load and save (admin only). |
| `app/Http/Controllers/ProposalController.php` | Modified | Shared `filterQuery()` (status incl. Expired, service, BDE, client, industry, location, dates); export. The dashboard is rewritten on live filtered data: KPIs, expired count, conversion rates, 6-month trend. Detail page uses real stage dates, emails, versions and audit. New actions: notes, complete follow-up, duplicate, share link. Ownership checks on update, delete and status changes; admin-only approve, reject and decision. Approving now sets the proposal back to "Draft — ready to send" rather than falsely marking it "Sent". |
| `app/Http/Controllers/DashboardController.php` | Modified | My Proposal Dashboard now has a real "My Proposals" table (search, status and service filters, pagination, actions) and a follow-ups panel. |
| `resources/views/pages/create-proposal.blade.php` | **Replaced** | Every field was a hard-coded `value="…"` with no form or name attributes. It is now one form that auto-fills from the chosen requirement or opportunity and saves. |
| `resources/views/pages/commercial-calculator.blade.php` | **Replaced** | Static numbers became a live JS calculator plus a server-side save (percent-of-CTC or per-employee models). |
| `resources/views/pages/proposal-editor.blade.php` | **Replaced** | Inputs bound to the proposal; locked fields shown read-only for BDEs; template select; working Save Draft and Preview buttons. |
| `resources/views/pages/proposal-preview.blade.php` | **Replaced** | Renders the real document. Save Draft, Generate PDF, Send Email and Cancel all work; previously they were toasts and one broken link (`compose-email.html`). |
| `resources/views/pages/compose-email.blade.php` | **Replaced** | The page crashed (unescaped `{{client_name}}` merge tags in Blade). Now a real send form with template switching, previous-email history and approval blocking. |
| `resources/views/pages/email-tracking.blade.php` | **Replaced** | Was 3 hard-coded rows. Now live data, KPIs, filters, per-email event log, export, and an honest explanation of limitations. |
| `resources/views/pages/proposal-versions.blade.php` | **Replaced** | Was static V1–V3 rows with toast "Compare" buttons. |
| `resources/views/pages/proposal-templates.blade.php` | **Replaced** | Was static cards with dead Edit buttons. Now DB cards plus a working Edit / New modal. |
| `resources/views/pages/proposal-field-permissions.blade.php` | **Replaced** | Redesigned: grouped rows, icons, switch toggles, live badges, responsive layout. Now saves per template. |
| `resources/views/pages/proposal-detail.blade.php` | **Replaced** | Summary, scope, stage bar, email tracking, follow-up, versions and audit were all hard-coded. Every button now works. Download PDF, Share Link, Duplicate, Mark Lost and Previous / Next were ported in from the old static `proposals` page. |
| `resources/views/pages/proposal-dashboard.blade.php` | **Replaced** | Fake filters ("Apply Filters" was a toast), fake trend bars, fake 13.1% / 87.5% conversion and "vs last period" labels replaced with real data. |
| `resources/views/pages/bde-dashboard.blade.php` | **Replaced** | Added the proposal listing; KPIs link to filtered lists. |
| `resources/views/pages/all-proposals.blade.php` | Modified | Status, service and BDE filters (were static); export; "Edit in wizard", PDF and versions actions. Quick-edit modal now lists every existing service; before, editing a proposal whose service wasn't in the list silently blanked it. Delete only offered where allowed. |
| `resources/views/pages/approval-queue.blade.php` | Modified | The decision card at the bottom was dead inputs; it is now a working form. Approve / Reject are admin only. The custom rate is shown. |
| `resources/views/pages/email-settings.blade.php` | Modified | "Connected" and SPF / DKIM / DMARC "Verified" badges were hard-coded. They now show the actual mailer, or "Check DNS". |
| `resources/views/pages/proposal-picker.blade.php`, `proposal-public.blade.php` | **New** | Picker for wizard steps opened without a proposal; public read-only shared proposal page. |
| `resources/views/partials/proposal-stepper.blade.php`, `partials/proposal-document.blade.php`, `pdf/proposal.blade.php` | **New** | Shared stepper; the one proposal document used by the preview, PDF and share page; dompdf wrapper. |
| `resources/views/pages/proposals.blade.php` | **Deleted** | A static mock of a single proposal. Its actions were moved to the real Proposal Detail page, and the `/proposals` URL now opens the proposal list. |

### Other fixes
| File | Type | What / why |
|---|---|---|
| `resources/views/pages/unified-dashboard.blade.php` | Modified (2 lines) | Fixed a Blade syntax error (`@endforeach@if`) that made the page return a 500 error. |
| `resources/views/auth/login.blade.php` | Modified (1 line) | Fixed the broken logo `src=\"…\"` escaping. |
| `composer.json` | Modified | Added `barryvdh/laravel-dompdf`. |
| `README.md` | Modified | Setup and upgrade steps; updated module list. |

## Email tracking — how it works now
1. **Sending.** Compose Email sends the message through Laravel Mail, using the `MAIL_*` settings in `.env` and the Email Settings sender address. The proposal PDF is attached.
2. **Records.** Every send is stored in `proposal_emails`: proposal, version, recipients, subject, body, unique token, Sent or Failed status (with the error message), and who sent it.
3. **Opens.** A 1×1 tracking GIF (`/t/o/{token}.gif`) is embedded in the email. Each time it loads, an `open` event is stored and the email's first open, last open and open count are updated.
4. **Clicks.** Every URL in the body, plus a "View proposal online" button, is rewritten to a *signed* redirect (`/t/c/{token}`). The signature means it cannot be abused as an open redirect. Each click is recorded, then the user is redirected to the real link.
5. **Lifecycle.** A successful send marks the proposal Sent, stores `sent_at`, schedules the first follow-up (Proposal Master "Follow-up 1") and creates a Follow-up record.

**What was broken before:** nothing was sent or tracked at all. The Send button was a "queued" toast; the tracking page, the detail page's tracking panel and the Email Settings "Connected" / "Verified" badges were all hard-coded. The compose page itself crashed.

**Limitations (opens are not 100% accurate):**
- **Opens depend on images loading.** Clients that block images record no open.
- **False opens.** Apple Mail Privacy Protection and some security scanners pre-fetch images or links, which can record opens or clicks that no person made.
- **"Sent" is not "delivered".** It only means the mail server accepted the message. Delivery and bounce tracking would need provider webhooks, which are not configured.
- **Some setup is required.** Tracking only works with a real mailer and a publicly reachable `APP_URL`.

## Known limitations / not in scope
- **Other static pages.** Negotiation, Client 360, Calendar, Reports, MIS Reports, Collection Report, Revenue, Notifications, Saved Filters and the Closure / Onboarding / Recruitment / Agreement forms were not in the brief and still show sample data. The Collection Report and MIS Reports "Export" buttons call `showToast()`, which is not defined anywhere, so clicking them throws a JavaScript error.
- **Notification badge.** The "12" in the top bar is hard-coded.
- **Cropped logo.** `public/assets/logo-dark.png` is itself cropped (it includes a hamburger icon and cut-off text). It appears that way on the proposal preview and PDF. Replace the file to fix it.
- **Not run on MySQL.** The code is written for MySQL (no SQLite-only syntax), but has only been run on SQLite.

---

# Phase 2 — BDE Portal & role-based permission management

## Design decisions
- **No separate login system.** BDE accounts use the existing `users` table (`role = 'BDE'`, `is_active`, `mobile`) and the existing login. The app's auth already lives there, so a separate `bdes` table would have duplicated it.
- **New tables:**
  - `modules` — one row per permissionable module (`module_key` is unique; `status` turns a module on or off).
  - `bde_permissions` — one row per BDE per module, with `can_view`, `can_create`, `can_edit` and `can_delete`. It has foreign keys to `users` and `modules` and a unique key on (`user_id`, `module_id`).
  - Adding a future module means adding a `modules` row plus its route mapping. No redesign is needed.
- **Which roles are affected.** Only users with role **BDE** are governed by the matrix. Superadmin, Admin and Manager are unaffected. Existing admin-only rules still apply on top of BDE permissions; for example, BDEs still cannot approve proposals or edit templates or field permissions.
- **View is required for any access.** Create, Edit or Delete without View are dropped when saved, and are also rejected when checked.

## How enforcement works
1. **Backend (the real control).**
   - **Every route is checked.** The `module` middleware runs on every authenticated route. It maps the route name to a (module, action) pair using `config/modules.php` and returns **HTTP 403** if the BDE isn't allowed; JSON requests get a JSON 403.
   - **Unmapped routes are closed.** Any route not listed in the config is denied to BDEs. This includes BDE Management, so a BDE can never change their own or anyone else's permissions.
   - **Context-aware checks.** Some pages count as different actions depending on context. For example, the Create Proposal page opened with `?proposal=` counts as Edit, and the Invoice form opened with `?id=` counts as Edit.
   - **Changes apply on the next request.** Permissions are read from the database on every request, so a change takes effect on the BDE's next page load with no logout or redeploy.
2. **Inactive accounts.**
   - **Login.** An inactive account is refused even with the correct password.
   - **Existing sessions.** The `active` middleware signs out an already-logged-in BDE on their next request once they are deactivated.
   - **Remember-me logins.** Deactivating an account or resetting its password also rotates the account's remember-me token, which ends those logins.
3. **Frontend.**
   - **Sidebar.** Menus, and whole sections, only show what the BDE can view. Topbar links are gated the same way.
   - **Buttons.** `@perm('module','action')` hides Add, Import, Edit, Delete and status buttons in every list and detail page.
   - **Dashboard.** BDEs get their own dashboard: their assigned leads and lead stats, follow-up reminders, pending tasks, recent activities, open pipeline and proposal performance. Each widget only appears if the BDE has View on that module.
   - **No data leaks through other pages.** Company, Lead and Opportunity detail pages and global search hide related records from modules the BDE can't view.

## Files (phase 2)
| File | Type | What / why |
|---|---|---|
| `config/modules.php` | **New** | Module list, route → (module, action) map, routes every signed-in user may open, and starter permissions for existing BDEs. |
| `database/migrations/2026_10_12_000002_create_bde_permissions.php` | **New** | `modules` and `bde_permissions` tables; seeds the modules; gives existing BDE users their previous access (no Delete) so they are not locked out by the upgrade. Rolls back cleanly. |
| `app/Models/Module.php`, `app/Models/BdePermission.php` | **New** | Models for the new tables. |
| `app/Models/User.php` | Modified | `isBde()`, `bdePermissions()`, `permissionMap()` (cached per request), `canDo(module, action)`, `canRoute(route)`. |
| `app/Http/Middleware/CheckModulePermission.php` | **New** | Server-side check on every route; returns 403. |
| `app/Http/Middleware/EnsureAccountActive.php` | **New** | Signs out deactivated accounts. |
| `app/Http/Middleware/EnsureAdmin.php` | **New** | Restricts BDE Management to administrators. |
| `bootstrap/app.php` | Modified | Registers the `module`, `active` and `admin` middleware aliases. |
| `routes/web.php` | Modified | Adds `active` and `module` to the protected group; adds the BDE Management routes (admin only); names the `/` route `home`. |
| `app/Http/Controllers/BdeController.php` | **New** | BDE accounts: list (search and status filter), create, profile, edit, activate/deactivate, password reset, permission matrix save. Validation: unique email, password policy, mobile format. Every account and permission change is written to the audit log with the admin's name and a timestamp. |
| `app/Http/Controllers/AuthController.php` | Modified | Refuses inactive accounts at login; records `last_login_at`; always sends BDEs to their dashboard. |
| `app/Http/Controllers/DashboardController.php` | Modified | Builds the BDE dashboard from permitted modules only. |
| `CompanyController.php`, `LeadController.php`, `OpportunityController.php`, `SearchController.php` | Modified | Hide related data from modules the BDE can't view. |
| `app/Providers/AppServiceProvider.php` | Modified | Registers the `@perm` Blade directive. |
| `database/seeders/DatabaseSeeder.php` | Modified | The demo BDE gets starter permissions. |
| `resources/views/pages/bde-management/{index,form,show,permissions}.blade.php` | **New** | BDE listing, create/edit form, profile with permission summary and history, and the permission page. |
| `resources/views/partials/permission-matrix.blade.php` | **New** | The permission matrix: checkboxes per module and action, Select All per column, per row and for everything, view dependency enforced in the UI, Reset and Cancel buttons, responsive. |
| `resources/views/pages/bde-home.blade.php` | **New** | The BDE dashboard. |
| `resources/views/errors/403.blade.php`, `errors/denied.blade.php` | **New** | A themed "Access denied" page. |
| `resources/views/layouts/app.blade.php` | Modified | Sidebar and topbar respect permissions; new "Administration → BDE Management" menu. |
| 22 page views (companies, contacts, leads, opportunities, requirements, clients, activities, followups, tasks, invoices, payments, all-proposals, bde-dashboard, proposal-detail, proposal-dashboard, proposal-preview, proposal-versions, service/commercial master, proposal-templates, lead/opportunity/company detail) | Modified (small) | Create, Edit, Delete and Import controls wrapped in `@perm(...)`. No other markup changed. |

## Verification (phase 2)
- **54 server-side checks** covering all 12 acceptance criteria:
  - creating an account
  - BDE login
  - different permission sets for different BDEs
  - View-only access
  - no-View access, including direct URLs and JSON requests (403)
  - Create, Edit and Delete each restricted independently
  - permission changes taking effect immediately
  - inactive accounts losing their session and being refused at login
  - Admin unaffected
  - controls bypassed by posting directly
  - permissions persisting after re-login
  - BDE unable to change their own permissions or create accounts
  - search not leaking restricted modules
- **Browser run (real CSRF tokens):**
  - Admin creates a BDE through the matrix UI, including Select-All behaviour.
  - The BDE logs in and sees only the permitted menus and buttons.
  - Opening `/invoices` directly as that BDE returns 403.
  - No JavaScript errors; the pages work at mobile width.
- **No regressions:** the earlier suites (53 CSV checks and 65 proposal checks) still pass.

---

# Phase 3 — Business Proposal Builder & Template Management

The full section-18 specification is in **`docs/PROPOSAL_BUILDER_SPEC.md`**:

- screens
- field list and validation
- calculation spec with examples
- PDF layout
- template UIs and the email trigger map
- workflow, schema, endpoints and permission matrix
- estimate and test/deploy checklist

Screenshots are in `docs/screens/`.

**Note on the design reference.** The "four-step UI design reference" named in the prompt was not attached. The screens follow the written spec and the existing CRM design system.

## Deploy steps (phase 3)
1. `composer update ezyang/htmlpurifier barryvdh/laravel-dompdf` — adds HTMLPurifier, which sanitises rich text.
2. **Back up the database**, then run `php artisan migrate`. The migration `2026_10_13_000001_create_proposal_builder` adds the tables and columns and does the following:
   - converts the existing templates into version 1
   - adds the "Standard Business Proposal" and "Custom Proposal Template" templates
   - seeds 6 email templates
   - gives existing BDEs their previous proposal abilities as capabilities

   It rolls back cleanly.
3. `APP_URL` and `MAIL_*` must be set for real sending and tracking. Optionally, fill Site Settings → email / contact number so `{{company_email}}` and `{{company_phone}}` resolve. Until then they are removed and shown as a warning, not as a blocker.
4. `storage/app/private` must be writable and must not be web-served. Sent PDFs and acceptance evidence are stored there.
5. Review each BDE's **Special permissions** in BDE Management.

## Key behaviours
- **Builder routes.** The four steps keep the old route names (`create-proposal`, `commercial-calculator`, `proposal-editor`, `proposal-preview`), so links and BDE permissions still work. Older proposals created by the wizard still open in their original preview and compose screens.
- **Commercials.** Each service is stored in `proposal_service_terms`. `ProposalCalculator` is the single source for the live preview, the PDF, the dashboard value and the mandate snapshot. Rates and estimates are shown separately, and no amount is invented when an input is missing. Amounts use Indian grouping.
- **Templates.**
  - Proposal and email templates are versioned.
  - Proposals copy the template content and record the template version, so template edits never change existing or sent proposals.
  - Unknown placeholders are rejected.
  - Locked company wording can only be edited by an admin.
- **Sending.**
  - Sending needs a confirmation step. Test sends go to the sender only.
  - An idempotency key prevents duplicate sends.
  - Failures are logged and can be retried in place, without a duplicate email or a status change.
  - A successful send stores the PDF and locks the version. Any change after that requires a revised version.
- **Acceptance and mandates.**
  - Acceptance is only recorded manually, with date, accepted-by, notes and an evidence file. Email opens never count.
  - Activating a mandate is protected by a lock, and the database allows one mandate per proposal.
  - Requirements are linked to an active mandate of the same company. They are never created automatically.
- **Status history.** Every status change is recorded with its reason. The detail-page timeline uses that history.

## Files (phase 3)

| File | Type | What / why |
|---|---|---|
| `config/proposal_builder.php` | new | Services, option lists, section definitions (locked / service-specific), placeholders, email modules/triggers. |
| `config/modules.php` | modified | New modules (mandates, proposal_templates, email_templates), special capabilities, legacy BDE extras, route → permission map for all new routes; email tracking now needs `view_communications`. |
| `composer.json` | modified | `ezyang/htmlpurifier`. |
| `database/migrations/2026_10_13_000001_create_proposal_builder.php` | new | Schema + data migration (see spec §8). |
| `database/seeders/DatabaseSeeder.php` | modified | Seeds BDE capability extras. |
| `app/Support/ProposalDefaults.php` | new | Default section wording and seeded email templates. |
| `app/Services/ProposalCalculator.php` | new | Commercial rules for Permanent/Staffing/Payroll + GST; INR formatting. |
| `app/Services/ProposalCommercialRules.php` | new | Conditional server-side validation, input cleaning, defaults. |
| `app/Services/ProposalDocument.php` | new | Builds content from template versions, resolves values, renders sections and commercial table. |
| `app/Services/PlaceholderRenderer.php` | new | Escaped placeholder replacement; removes and reports missing values. |
| `app/Services/HtmlSanitizer.php` | new | HTMLPurifier allow-list. |
| `app/Services/ProposalPdf.php` | new | dompdf rendering, private storage, immutable bytes for sent versions. |
| `app/Models/ProposalServiceTerm.php`, `ProposalTemplateVersion.php`, `EmailTemplate.php`, `EmailTemplateVersion.php`, `ProposalStatusHistory.php`, `Mandate.php`, `CommunicationLog.php` | new | Models for the new tables (template versions are immutable). |
| `app/Models/Proposal.php` | modified | New statuses, casts, relations, status-history hook, version locking, `contentEditable()`, extended snapshots, `edit_all_drafts`. |
| `app/Models/ProposalTemplate.php` | modified | Versions, `saveVersion()`, status, service matching. |
| `app/Models/ProposalVersion.php` | modified | `sent_at` cast. |
| `app/Models/User.php`, `BdePermission.php`, `Requirement.php` | modified | Capability checks (`extras`), mandate relation. |
| `app/Http/Controllers/ProposalBuilderController.php` | new (replaces `ProposalWizardController`) | Steps 1–4, live calculation, save as template, PDFs, readiness checks. |
| `app/Http/Controllers/ProposalWizardController.php` | **deleted** | Replaced by the builder. |
| `app/Http/Controllers/ProposalTemplateController.php` | rewritten | Template list/create/edit with versions, duplicate, status, preview, permissions. |
| `app/Http/Controllers/EmailTemplateController.php` | new | Email template management. |
| `app/Http/Controllers/EmailComposerController.php` | new | Client emails for Companies/Contacts/Leads with template dropdown + communication log. |
| `app/Http/Controllers/MandateController.php` | new | Record acceptance (+evidence), activate mandate, mandate list/detail/status. |
| `app/Http/Controllers/ProposalEmailController.php` | modified | Template-based send, confirmation, test send, idempotency, retry, PDF storage & version lock. |
| `app/Http/Controllers/ProposalController.php` | modified | Detail page data (services, acceptance, history), status changes with reasons, approve → Ready to Send, delete owner-only and blocked with a mandate, communications gated. |
| `app/Http/Controllers/ProposalVersionController.php` | modified | Revise/restore include services & content; blocked after acceptance; diff shows commercials & content. |
| `app/Http/Controllers/RequirementController.php` | modified | Mandate link validated (active, same company). |
| `app/Http/Controllers/BdeController.php` | modified | Saves special capabilities; audit shows capability changes. |
| `app/Http/Middleware/CheckModulePermission.php` | modified | Email composer routes check the source module. |
| `routes/web.php` | modified | Builder routes repointed; new template, email, acceptance, mandate routes. |
| `resources/views/pages/builder/step1–4.blade.php`, `builder/partials/calc.blade.php` | new | The four steps and calculation breakdown. |
| `resources/views/partials/builder-document.blade.php`, `pdf/proposal-builder.blade.php` | new | On-screen document and PDF layout (cover, page numbers, acceptance page). |
| `resources/views/pages/proposal-template-form.blade.php`, `proposal-template-preview.blade.php` | new | Template editor and preview. |
| `resources/views/pages/proposal-templates.blade.php` | rewritten | Template table with filters and actions. |
| `resources/views/pages/email-templates/{index,form,preview}.blade.php`, `email-compose.blade.php` | new | Email template management and client email composer. |
| `resources/views/pages/mandates/{index,show}.blade.php` | new | Mandates. |
| `resources/views/pages/create-proposal.blade.php`, `commercial-calculator.blade.php`, `proposal-editor.blade.php` | **deleted** | Old wizard screens, replaced by `builder/step1–3`. |
| `resources/views/pages/proposal-detail.blade.php` | modified | Services & commercials, acceptance & mandate panel, status history, status modal with reason. |
| `resources/views/pages/compose-email.blade.php`, `proposal-public.blade.php`, `proposal-versions.blade.php` | modified | Template dropdown/idempotency (legacy), builder document on the public link, sent badges and sent PDFs. |
| `resources/views/pages/requirements.blade.php` | modified | Mandate selector (filtered by company), pre-fill from mandate. |
| `resources/views/pages/company-detail.blade.php`, `lead-detail.blade.php`, `contacts.blade.php` | modified | "Send Email" (template dropdown) and "New Proposal" actions. |
| `resources/views/pages/all-proposals.blade.php`, `bde-dashboard.blade.php`, `bde-home.blade.php` | modified | Blade directive spacing fix; new modules on the BDE home. |
| `resources/views/partials/proposal-stepper.blade.php`, `permission-matrix.blade.php`, `pages/bde-management/show.blade.php`, `layouts/app.blade.php` | modified | 4-step stepper; Special permissions block; sidebar (Proposal Builder, Mandates, Email Templates; removed the old separate step links). |
| `docs/PROPOSAL_BUILDER_SPEC.md`, `docs/screens/*` | new | Section-18 deliverables. |

## Verification (phase 3)
- **Builder acceptance script: 95 checks, all passing.** It covers all 20 acceptance tests from the prompt:
  - creating a proposal for an existing client, with multiple services
  - required fields for each pricing model
  - permanent (% and fixed), staffing (components and fee base) and payroll (per-employee and minimum) calculations
  - GST inclusive and exempt
  - save and resume
  - the template dropdown, including inactive templates
  - template versioning
  - template changes never alter a sent proposal (stored PDF is byte-identical)
  - PDF content and page numbers
  - email templates and placeholders, edit before send, confirmation, and logs
  - acceptance with evidence, with no duplicates
  - exactly one mandate
  - requirement linking and validation
  - 403 for missing permissions and capabilities
  - SMTP failure, then a retry with no duplicate
- **Browser (Chromium), 25 checks, no console errors.** The checks cover:
  - the full 4-step flow with live calculation, TinyMCE editing, the send confirmation dialog and PDF download
  - mobile layout at 390 px with no horizontal scroll
  - admin template screens
  - recording acceptance and activating the mandate through the UI
- **Earlier suites still pass:**
  - The proposals/tracking/versions suite (54) was rewritten for the builder.
  - CSV (53) and BDE permissions (54) passed unchanged.
  - Smoke runs as admin and as BDE returned 200 on every page, or 403 where expected.
- **Bugs found and fixed by these tests:**
  - Conditional Step 2 fields never appeared.
  - An email template could load into TinyMCE before the editor was ready, leaving the message empty.
  - Inclusive GST did not show the value before GST.
  - "Edit all drafts" allowed deleting a colleague's proposal.
  - Step 3 could be skipped without content.
- **Not tested:** MySQL and real SMTP delivery. No MySQL server or mail server was available, so tests ran on SQLite, used the log mailer, and simulated the SMTP failure.

## Known limitations (phase 3)
- E-signature is not included. Acceptance is recorded manually with an evidence upload, and no paid service was added.
- Delivered/Viewed come from the existing pixel and click tracking. They are informational only, because many mail clients block tracking pixels.
- PDF fonts are dompdf's DejaVu Sans. The ₹ symbol renders correctly; custom brand fonts would need to be added to `storage/fonts`.
