feat/crm-ops #52

Merged
YousifShkara merged 3 commits from feat/crm-ops into main 2026-06-04 06:52:31 +02:00
Owner
No description provided.
CompanyListPage and ContactListPage both rendered a static set of
hardcoded rows (Acme Corp, TechStart Inc, Bob Johnson, etc.) when
the backend fetch failed or no token was present. On a fresh-tenant
session the API call returns 0 rows, which is a real empty state,
not a fall-back trigger; on an auth failure the AuthGuard layout
already redirects to /login before the page mounts. The Source::Demo
branch was unreachable for the empty-tenant case (its fall-through
fired only on auth failure, not empty data) but the dead 'Acme Corp'
rows kept showing up whenever someone exercised the no-token codepath
deliberately, e.g. during dev with cleared session state.

Replace both list pages with an explicit three-state render:
- loading: skeleton rows
- fetch failed (token absent or 5xx): an inline red error banner with
  a 'refresh to retry' hint, table empty
- success: live rows OR an empty-state pointing at the New button

CompanyRow loses the cosmetic 'Contract' column - contract linking is
its own story and the column rendered empty for every backend row.

Demo-mode plumbing removed:
- CompanySource / ContactSource enums
- All 5 seeded CompanyRow blocks (Acme Corp, TechStart Inc, Global
  Widgets, New Venture LLC, Dell Technologies)
- All 3 seeded ContactRow blocks (Bob Johnson, Alice Williams,
  Charlie Brown)

Detail pages (CompanyDetailPage, ContactDetailPage) are still fully
seeded - they get their own commit.
CompanyDetailPage previously made zero backend calls. The URL props.id
was used only for the page title; every other field (Acme Corp,
'(555) 123-4500', '123 Business Ave', Bob Johnson / Sarah Miller /
Mike Davis contacts, TKT-1234 + TKT-1231 tickets, 'Managed Services
Agreement' contract card, 'Revenue (YTD): $42,500') was a literal.

Rewire end-to-end:

- Detail header pulls the company name and three statistics
  (open_tickets, contacts, sites) from GET /v1/companies/{id} now
  that the rollup fields are populated.
- Contacts card -> GET /v1/companies/{id}/contacts; each row links
  to the contact detail page.
- Sites card -> GET /v1/companies/{id}/sites (new surface; was not
  visible anywhere in the UI). Read-only for this commit; the inline
  add/edit modal lands separately.
- Recent Tickets card -> GET /v1/tickets?company_id={id}&page_size=5&sort=-updated_at.
  Status badge color comes from TicketStatusSummary.is_closed.
- Sidebar Details card pulls type / phone / website / industry /
  address from the loaded CompanyResponse. Empty fields hide
  entirely rather than displaying placeholder text.
- Contract sidebar card removed (separate story); 'Revenue (YTD)'
  stat removed (no backend source).
- Three async resources fire in parallel on mount; each renders
  loading skeleton, error message, empty state, or rows
  independently. Tenant-generation read keeps them re-fetching on
  org switch.

New surfaces:

- Edit button on the detail header navigates to /companies/:id/edit.
- Delete button triggers a window.confirm + DELETE /v1/companies/{id};
  on success the user lands back on the list. ButtonVariant::Danger.
- CompanyForm component is now shared between New and Edit, with a
  CompanyFormMode discriminator. Fields expand beyond the original
  name + type: industry, website, phone, full address (line1, line2,
  city, state, postal_code, country) - all optional except name.
  Form errors surface inline rather than via console.error.

Wire bits:

- New Route::CompanyEdit { id } in lib.rs route table + matching
  component handler.
- CompanyEditPage fetches the existing company first, then mounts
  CompanyForm with the loaded values; 404 surfaces an inline error
  card with a back-to-list link.

Remaining seeded content lives in ContactDetailPage and the contact
new form's company picker; those get separate commits.
feat(crm): rewrite contact pages on live backend, add CompanyPicker, Edit, portal toggle
All checks were successful
Create release / Create release from merged PR (pull_request) Has been skipped
Check / clippy + fmt + tests (pull_request) Successful in 34s
cd01007e0c
ContactNewPage previously shipped a hardcoded company dropdown
([("1","Acme Corp"),("2","TechStart Inc"),("3","Global Widgets")])
that parsed unknown values to Uuid::nil() so the server rejected
them with a typed validation error. The form was functionally
broken; the only path to a successful create was hand-pasting a
real company UUID into one of the fake values. The detail page
went further: zero backend calls, every field hardcoded (Bob
Johnson, bob@acme.com, '(555) 123-4567', '(555) 987-6543',
Acme Corp link, 'Created ticket TKT-1234 - 2 hours ago' activity
entries, fake 'Portal Access: Active - Today, 9:15 AM' card).

Replace end to end.

CompanyPicker (new components/company_picker.rs):
- Search-input that hits GET /v1/companies?q=...&page_size=20.
- Renders results as a click-to-select dropdown; on pick fires
  onselect((id, name)).
- When a selection exists, switches to a 'selected chip + Change'
  layout so the caller knows what's set without re-running the
  search.
- Tiny inline percent-encoder for the few chars that break URL
  query strings; avoids pulling in urlencoding for one call site.
- Reusable: contact form uses it today, ticket new form / time
  entry form will reuse it.

ContactForm (new):
- Shared between ContactNewPage and the new ContactEditPage.
- Expands the field set to match the backend's CreateContactRequest /
  UpdateContactRequest: first_name + last_name (required), email,
  phone, mobile, title, department, contact_type (primary /
  technical / billing / other), plus the CompanyPicker.
- nil() company guard now lives client-side: submit aborts with an
  inline error if no UUID is set, instead of round-tripping to the
  server for a validation error.
- Errors surface inline via a red banner above the fields.

ContactEditPage + Route::ContactEdit:
- /contacts/:id/edit route + dispatch.
- Fetches the contact first via GET /v1/contacts/{id}, then mounts
  ContactForm with the loaded values. CompanyPicker initializes to
  the linked company so the user can leave it or pick another.

ContactDetailPage rewrite:
- Two parallel resources: GET /v1/contacts/{id} for the sidebar
  Contact Information card; GET /v1/tickets?contact_id={id}&page_size=5
  for the Recent Tickets card.
- Sidebar renders every optional field only when populated; no more
  placeholder values for missing data.
- Company name + link come from the loaded ContactResponse
  (company_id + company_name).
- Edit and Delete actions on the page header; Delete confirms via
  window.confirm and DELETE /v1/contacts/{id}.

Portal Access card (per docs/mokosh-upgrade/CRM/02-contacts.md D-A):
- Reads is_portal_user off the contact; shows Granted / Not granted
  badges instead of the fake 'Active' + 'Last Login: Today, 9:15 AM'.
- Grant button PUTs {is_portal_user: true}; Revoke button PUTs
  {is_portal_user: false}.
- Both states surface the password-mint caveat: the flag flip alone
  does not let the contact sign in; password issuance lives behind
  Settings > Portal Users (separate story).
- On toggle the detail resource restarts so the card re-renders
  from authoritative state.

Wire bits:
- New Route::ContactEdit { id } in lib.rs + ContactEdit component
  wrapper.
- Removed every hardcoded TKT-/Bob Johnson/Sarah Miller/Mike Davis/
  Acme Corp string from the contact pages.
- humanize_contact_type helper for the Type badge.

grep -nE 'Acme Corp|TechStart|Global Widgets|Bob Johnson|Sarah Miller|Mike Davis|Alice Williams|Charlie Brown|bob@acme.com|sarah@acme.com|mike@acme.com|TKT-1234|TKT-1231|TKT-1233' src/pages/contacts.rs returns zero results.
YousifShkara deleted branch feat/crm-ops 2026-06-04 06:52:31 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/mokosh-apps!52
No description provided.