feat/crm-ops #55

Merged
YousifShkara merged 3 commits from feat/crm-ops into main 2026-06-04 08:12:21 +02:00
Owner
No description provided.
The CompanySitesCard rendered the sites list read-only - the backend
has full CRUD (POST/PUT/DELETE /v1/contacts/sites) but the only way
to create or edit a site was a hand-rolled curl. Wire the modal:

- 'New Site' action button on the card header opens an empty modal
  pre-populated with the current company_id.
- Each site name in the rows is now a button that opens the same
  modal pre-filled with that site's data (edit mode).
- Form fields cover the editable set of CreateSiteRequest /
  UpdateSiteRequest: name (required), full address (line1, line2,
  city, state, postal_code, country), phone, timezone, is_primary
  checkbox.
- Save POSTs to /contacts/sites in create mode or PUTs to
  /contacts/sites/{id} in edit mode. On success the sites resource
  restarts so the card re-renders with the new state.
- Edit mode adds a Delete button in the modal footer (window.confirm
  guard) that hits DELETE /contacts/sites/{id}; the card refreshes
  the same way.

SiteSummary gains phone + timezone so the existing list response
populates the edit form without a follow-up fetch. notes / latitude
/ longitude stay off the surface for now; can be added when
geofencing or service-area work lands.

CompanySitesCard now takes company_id as a prop so the create path
knows which company to link to. CompanyDetailPage threads
company_id_str through to the card.
Both list pages were client-side over the first page returned: the
backend sent up to 25 rows, the client then paginated and filtered
THAT slice. With a tenant of more than 25 companies / contacts you
saw only the first page's subset and the page counter capped at 1.
The 'Type' filter on Companies and the search box on Contacts were
both client-only too - empty query never round-tripped 'company_type'
or 'q' to the server.

Push every dimension to the backend.

Companies list:
- Build the path as
  /contacts/companies?page=N&per_page=25&q=...&company_type=...&sort=...&sort_dir=...
  and let the backend filter + paginate + sort.
- Pass company_sort_query() => ('name'|'company_type', 'asc'|'desc')
  from the table-header sort state.
- Total page count reads PaginatedResponse.meta.total instead of the
  client-side filtered length.
- Search and Type onchange handlers reset page to 1 so filtering
  from page 5 doesn't leave you stranded with zero matches on a
  no-longer-existent page.

Contacts list:
- Same pattern, with the addition of two new filter dropdowns the
  doc 02 A.4 listed:
  - contact_type filter (Primary / Technical / Billing / Other)
  - is_portal_user filter (Portal users only / Non-portal only)
- contact_sort_query() => ('last_name'|'company_name', 'asc'|'desc').

PaginatedCompanies / PaginatedContacts gain a meta field deserialized
from PaginationResponse.meta; only .total is read for now, the rest
of the meta (has_next/has_prev) is still derived from page+total at
the DataTable level.

Wrong query-param name fix: every '?page_size=' across the file plus
the CompanyPicker was '?per_page=' on the server. Renamed eight call
sites (list pages, sites card, ticket sub-resources, picker). Only
the picker was visibly affected before this change because the server
silently fell back to default per_page=25; the rest were already
asking for the default.

Helpers:
- company_sort_query / contact_sort_query map the SortKey enums to
  the column names the server's PaginationParams.sort understands.
- urlencoding_minimal: tiny percent-encoder for query values; same
  function the picker already had, hoisted to file scope so the
  list pages can use it without re-importing.
feat(crm): prefill CompanyPicker on /contacts/new from ?company_id= query
All checks were successful
Create release / Create release from merged PR (pull_request) Has been skipped
Check / clippy + fmt + tests (pull_request) Successful in 53s
1aac7246e5
The CompanyContactsCard on the company-detail page had an 'Add Contact'
affordance that pointed at /contacts/new and dropped the user into an
empty form. They then had to retype the company name into the picker
they just clicked away from.

Make the add-contact flow round-trip the company:
- CompanyContactsCard takes the parent company's id + name as props
  and builds an href as
  /contacts/new?company_id={id}&company_name={name}.
- Switched the affordance from a routed Link to a plain <a href>
  because Dioxus' Link only accepts a Route enum value and the Route
  enum doesn't carry query params for ContactNew. The router still
  intercepts same-origin anchor clicks so navigation stays SPA.
- ContactNewPage reads window.location.search via web_sys on mount,
  parses company_id + company_name with UrlSearchParams, validates
  the id as a UUID, and seeds the ContactForm's CompanyPicker with
  the prefilled selection. The picker then renders as the 'selected
  chip + Change' layout it already had for the edit path, so the
  user can override if they came from the wrong company by mistake.
- An invalid or missing company_id falls back to an empty form -
  there is no error condition, just an unprefilled CompanyPicker.

This closes the 'add contact from company detail' loop without
having to refactor every Link { to: Route::ContactNew {} } across
the app or thread a typed query param through the Dioxus Route
enum.
YousifShkara deleted branch feat/crm-ops 2026-06-04 08:12:21 +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!55
No description provided.