Skip to content

Partner Country Executives UI

Frontend implementation of the Country Assignments section and the bulk-assign action embedded in the existing Partners list.

Routes & Entry Points

The module ships two cooperating UI surfaces:

js
// frontend/client/src/routes/index.js
{
    prefix: '/partners',
    layout: 'dashboard',
    children: ['', ':id/clients', /* ... */, 'crm', 'country-assignments'],
}
RouteComponentPurpose
/partners/country-assignmentsviews/partners/country-assignments/index.vueStandalone page with PAE and SDR tabs to manage executives
/partnersviews/partners/index.vueExisting partners list, now hosting the bulk-assign offcanvas

The Country Assignments page is registered as a child of the existing Partners route so it appears alongside /partners, /partners/payments, /partners/crm, etc.

File Tree

frontend/client/src/
├── routes/index.js                                                  ← /partners/country-assignments registered
├── services/
│   ├── index.js                                                      ← registers partnerCountryExecutives + partner.bulk*
│   ├── partner.service.js                                            ← + bulkAssignAdministrator / bulkAssignSalesman
│   └── partnerCountryExecutives.service.js                           ← new: list/get/create/update/remove/listActive/...
└── views/
    └── partners/
        ├── index.vue                                                 ← hosts BulkAssignExecutiveOffcanvas
        ├── components/
        │   ├── Table.vue                                             ← + bulk action buttons (PAE / Salesman)
        │   └── BulkAssignExecutiveOffcanvas.vue                      ← new: shared by both bulk actions
        └── country-assignments/
            ├── index.vue                                             ← page root + tabs (PAE / SDR)
            ├── constants/index.js                                    ← ROLE_TYPE + STATUS_OPTIONS
            └── components/
                ├── AssignmentFormOffcanvas.vue                        ← create/edit panel (role-agnostic)
                ├── PaeAssignmentsTable.vue                            ← PAE-specific datatable
                ├── SdrAssignmentsTable.vue                            ← Salesman/SDR-specific datatable
                └── CountryChips.vue                                   ← country pill list + fallback chip

Page Layout: Country Assignments

views/partners/country-assignments/index.vue renders a SectionTitle and a Tabs component with two tabs:

Tab keyComponentBacking role type
paePaeAssignmentsTable.vue'pae'
sdrSdrAssignmentsTable.vue'salesman'

Both tabs share the same shape and almost identical logic — they differ only in the role type passed to the API and the i18n namespace they consume (countryAssignments.pae.* vs countryAssignments.sdr.*).

Tab content

Each table renders, top to bottom:

  1. No-fallback warning banner — visible while getGlobalFallback(role) returns null. Tells the operator that there is currently no global fallback configured for the role type.
  2. Description banner — static info block explaining what this tab does.
  3. Activate button — opens AssignmentFormOffcanvas in create mode for the role.
  4. DataTable — paginated list of executives for that role type with filters (administrator_id / name / email, country, status) and per-row actions (View / Delete).

Role-agnostic table component

Both PaeAssignmentsTable.vue and SdrAssignmentsTable.vue render almost identical templates. They are kept as separate files (rather than parameterized) so the i18n keys, role-specific copy and per-tab tweaks (e.g. Status color) remain easy to scan and customize.

Component Responsibilities

country-assignments/index.vue

  • Mounts the Tabs component with two static tabs (pae, sdr).
  • No data fetching — each tab is self-sufficient and lazy-renders its content the first time it is shown.

country-assignments/components/PaeAssignmentsTable.vue / SdrAssignmentsTable.vue

Each table:

  • Loads the locales catalog (api.catalog.getLocales) for the country filter dropdown and for CountryChips.
  • Loads the global fallback (api.partnerCountryExecutives.getGlobalFallback(role)) on mount and after every save / delete. The presence of a fallback drives the warning banner.
  • Loads the table data through the shared DataTable loadData prop, calling api.partnerCountryExecutives.list({ ...filters, role_type }).
  • Shows a per-row View button that opens AssignmentFormOffcanvas in edit mode pre-populated with the row, and a Delete action under the row's dropdown.
  • Both tables include a permission gate (canManage) controlled by ENFORCE_PERMISSIONS. The flag is set to false here for the same reason as in the backend: the manage-partner-country-executives permission is not yet seeded. Flipping it to true re-enables the gate without further changes.

country-assignments/components/AssignmentFormOffcanvas.vue

Role-agnostic panel for both create and edit, opened with show() (create) or show(item) (edit).

BehaviorDetail
Header copyPicked from `countryAssignments.offcanvas.{pae
CatalogsOn show() it fires api.catalog.getLocales() always, and api.partnerCountryExecutives.getAvailableAdministrators(role) only in create mode.
Edit-mode admin dropdownThe available-administrators endpoint excludes the current admin (since they already have an assignment). The component prepends the current admin manually so it stays visible — the field is disabled in edit mode.
Fieldsadministrator_id (select), country_ids (multi-select / tags), is_global_fallback (switch), and active (switch, only in edit mode).
Fallback warningWhen is_global_fallback is on, an alert reminds the operator that any partner not matched to a specific country will be routed to this executive.
SubmitCalls create(payload) or update(id, payload) and emits @saved so the parent table can reload() and re-fetch the global fallback.

The form transforms the UI's country_ids array into the API's locale_ids field at the boundary so the rest of the page only needs to know about "countries".

country-assignments/components/CountryChips.vue

Stateless presentation component. Renders one pill per country (with the country flag svg from ${env.S3_PATH}/uploads/locales/<code>.svg) plus an optional "Global fallback" chip. Falls back to a "No countries" muted label when both lists are empty.

Bulk Assignment Integration (Partners list)

The Partners table (views/partners/components/Table.vue) already supports row selection. Two new bulk action buttons are rendered above the table when auth.can('update-partner-administrator') is true:

ButtonCallsEndpoint
Asignar PAEopenBulkAssign('pae')POST /partners/bulk-assign-administrator
Asignar SalesmanopenBulkAssign('salesman')POST /partners/bulk-assign-salesman

The button collects table.itemsSelected (Vue ref or array, depending on the DataTable implementation), guards against an empty selection, and emits openBulkAssign with { role, partners } to the parent (views/partners/index.vue), which forwards the payload to the offcanvas.

components/BulkAssignExecutiveOffcanvas.vue

A single offcanvas covers both PAE and Salesman bulk operations:

  • On show({ role, partners }) it fetches api.partnerCountryExecutives.listActive(role). The dropdown options are built from the response, with the global fallback (if any) appearing first thanks to the backend ordering (computed is_global_fallback DESC, u.name ASC).
  • The selected partners are rendered as a compact, scrollable card so the operator can verify the scope of the action before confirming.
  • On submit it calls api.partner.bulkAssignAdministrator(payload) (PAE) or api.partner.bulkAssignSalesman(payload) (Salesman). The success toast surfaces both updated.length and skipped.length from the response so the operator knows how many partners were no-ops.

Source of truth

The dropdown is intentionally fed by the active executives (administrators with status_leads = 1 and the matching role), not by all administrators with the right role. This guarantees that the operator can only assign partners to administrators that have been explicitly enabled in the Country Assignments section (or via the administrator edit toggle below).

Activation toggle on the Administrator edit modal

views/administratorsV2/components/administrators/EditAdministrator.modal.vue exposes the existing status_leads field (leads assignment), which now doubles as the executive registry flag for partner-referral roles. Because it is the same column, toggling it here (or via the administrators table toggle, permission administrators-edit-leads) has the same effect as activating/pausing the executive from the Country Assignments section. There is no longer a separate allow_partner_referral_assignment field.

Service Layer

services/partnerCountryExecutives.service.js

MethodHTTPEndpoint
list(filters)GET/partner-country-executives
getById(id)GET/partner-country-executives/{id}
getAvailableAdministrators(roleType)GET/partner-country-executives/available-administrators
getGlobalFallback(roleType)GET/partner-country-executives/global-fallback
listActive(roleType)GET/partner-country-executives/active
create(payload)POST/partner-country-executives
update(id, payload)PUT/partner-country-executives/{id}
remove(id)DELETE/partner-country-executives/{id}

list calls resolveTableParams(filters) so DataTable filters are translated into the query parameters expected by the backend (start, length, sortBy, sortType, plus the module-specific filters).

services/partner.service.js (additions)

MethodHTTPEndpoint
bulkAssignAdministrator(payload)POST/partners/bulk-assign-administrator
bulkAssignSalesman(payload)POST/partners/bulk-assign-salesman

Both methods are thin wrappers over axios.post; the global response interceptor unwraps response.data so the caller receives { updated, skipped } directly.

Permission Gates

GateComponentCheck
Show Activate PAE buttonPaeAssignmentsTable.vuecanManage = ENFORCE_PERMISSIONS ? auth.can('manage-partner-country-executives') : true
Show Activate SDR buttonSdrAssignmentsTable.vuesame as above
Show Delete row actionboth tablessame as above
Show Asignar PAE / Asignar Salesman bulk buttonsviews/partners/components/Table.vueauth.can('update-partner-administrator')
Backend list / getGET /partner-country-executives*view-partner-country-executives (currently bypassed)
Backend writePOST/PUT/DELETE /partner-country-executives*manage-partner-country-executives (currently bypassed)
Backend bulk-assignPOST /partners/bulk-assign-*update-partner-administrator (enforced)

Temporary permission bypass

Both the routes and the UI ship with a const ENFORCE_PERMISSIONS = false flag. Once view-partner-country-executives and manage-partner-country-executives are seeded in catalog_admin_permissions, flip both flags to true. No other code change is needed: the permission names are already wired to permissionMiddleware.canByName (backend) and auth.can() (frontend).

i18n keys (countryAssignments.*)

The Country Assignments page uses a single i18n namespace shared across both tabs, with role-specific sub-namespaces for any text that diverges between PAE and SDR:

  • countryAssignments.title / subtitle / tabs.{pae|sdr} — page chrome.
  • countryAssignments.role.{pae|salesman} — role label rendered inside row chips.
  • countryAssignments.{pae|sdr}.activateBtn / noFallbackTitle / noFallbackDescription / descriptionTitle / description / deleteFallbackBlocked / deleteSuccess / activateSuccess — role-specific copy.
  • countryAssignments.offcanvas.{pae|sdr}.{create|edit}{Title|Subtitle} — header copy in AssignmentFormOffcanvas.
  • countryAssignments.offcanvas.{adminLabel|adminPlaceholder|countriesLabel|countriesPlaceholder|fallbackLabel|fallbackHint|fallbackWarning|activeLabel|activeHint|loadingCatalogs|updateSuccess} — shared form labels.
  • countryAssignments.columns.{adminId|executive|role|countries|status|actions} — DataTable headers.
  • countryAssignments.filters.{searchById|searchByName|searchByEmail|country|status} — DataTable filters.
  • countryAssignments.{viewInfoBtn|deleteConfirm|fallbackTooltip|fallbackChip|noCountries|status.{active|inactive}} — shared chrome.

The bulk-assign offcanvas uses the partners.bulkAssign.* namespace (same role split via partners.bulkAssign.{pae|salesman}.*).

Envia Admin