/** * Rider partners — the companies that supply riders. * * ── Why this page did not exist ───────────────────────────────────────────── * * `getpartners` has always been readable and nothing on the platform could * create a partner, so the five that exist were inserted by hand — two are * still called "Test". Meanwhile 125 of 200 merchants already carry a * `partnerid`, and one partner supplies 48 shops while another supplies 63. The * relationship the whole delivery side rests on was real, live and unmanaged. * * ── What onboarding a partner records ─────────────────────────────────────── * * Three things, and the last two are why the assign screen works at all: * * the district `partnerinfo.applocationid`, and `partnerlocations` beside * it. Every rider query joins through that id, so it has to be * a district Nearle actually services — see * `tamilNaduDistricts.ts` for why all 38 are shown anyway. * the merchant `tenants.partnerid`. This is what the assign screen reads to * decide whether to offer a partner tab at all. * the branch `tenantlocations.partnerid`. Which outlet they cover. * * A partner can also be attached to a merchant afterwards from that merchant's * own page — see `StoreDetailPage` — which is the ordinary case of a shop * changing partner without anybody re-onboarding the company. */ import { useMemo, useState } from 'react'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { Badge } from '@astryxdesign/core/Badge'; import { Button } from '@astryxdesign/core/Button'; import { Card } from '@astryxdesign/core/Card'; import { HStack } from '@astryxdesign/core/HStack'; import { Table, type TableColumn } from '@astryxdesign/core/Table'; import { Text } from '@astryxdesign/core/Text'; import { TextInput } from '@astryxdesign/core/TextInput'; import { VStack } from '@astryxdesign/core/VStack'; import { Bike, Plus, Truck } from 'lucide-react'; import { errorMessage } from '@/api/client'; import { partnersApi, type NewPartner, type Partner } from '@/api/deliveries'; import { tenantsApi } from '@/api/tenants'; import type { TenantInfo } from '@/api/types'; import { Selector } from '@astryxdesign/core/Selector'; import { districtOptions, isRunning, matchDistrict } from '../tamilNaduDistricts'; import { DataState } from '@/components/DataState'; import { PageHeader } from '@/components/PageHeader'; import { TablePager } from '@/components/TablePager'; import { usePaged } from '@/components/usePaged'; import { useAllPartners, useAppRegions, usePartnerRiderCounts, useTenantLocations, useTenants, } from '@/queries/hooks'; import { queryKeys } from '@/queries/keys'; import { Drawer } from '@/features/store-admin/Drawer'; import { PartnerRidersDrawer } from '../PartnerRidersDrawer'; import { DrawerButton } from '@/features/store-admin/drawerKit'; interface PartnerRow extends Record { partnerid: number; partnername: string; companyname: string; region: string; contact: string; status: string; /** How many riders they have. `null` while the count is still being read. */ riders: number | null; } export function PartnersPage() { const partners = useAllPartners(); const regions = useAppRegions(); const [editing, setEditing] = useState(null); /** The partner whose riders are on screen, if any. */ const [ridersFor, setRidersFor] = useState(null); /* The fleet size per partner, read alongside the directory. Without it the Riders button is a door with nothing written on it. */ const riderCounts = usePartnerRiderCounts(partners.data.map((entry) => entry.partnerid)); const regionName = useMemo(() => { const map = new Map(); for (const region of regions.data ?? []) { map.set(region.applocationid, region.locationname ?? `Region ${region.applocationid}`); } return map; }, [regions.data]); const rows = useMemo( () => partners.data.map((partner) => ({ partnerid: partner.partnerid, partnername: partner.partnername ?? `Partner ${partner.partnerid}`, companyname: partner.companyname ?? '', region: regionName.get(partner.applocationid ?? 0) ?? '—', contact: partner.primarycontact || partner.contactno || '', status: partner.status || 'Active', riders: riderCounts.get(partner.partnerid) ?? null, })), [partners.data, regionName, riderCounts], ); const paged = usePaged(rows); const columns: TableColumn[] = [ { key: 'partnername', header: 'Partner', width: { type: 'proportional', value: 3 }, renderCell: (row) => ( {row.partnername} {row.companyname ? ( {row.companyname} ) : null} ), }, { key: 'region', header: 'Home region', width: { type: 'proportional', value: 2 }, renderCell: (row) => ( {row.region} ), }, { key: 'contact', header: 'Contact', width: { type: 'proportional', value: 2 }, renderCell: (row) => ( {row.contact || '—'} ), }, { key: 'status', header: 'Status', align: 'end', width: { type: 'pixel', value: 110 }, renderCell: (row) => ( ), }, { /* Both actions in ONE column with a header, rather than two unlabelled ones. The riders button carries the fleet size, because "Riders" alone asks you to open a drawer to learn whether there are any — and the answer is the reason you would open it. */ key: 'actions', header: 'Fleet', align: 'end', width: { type: 'pixel', value: 184 }, renderCell: (row) => ( ); })} {shown.length === 0 ? ( No district matches “{districtSearch}”. ) : null} {chosenDistrict ? isRunning(chosenDistrict) ? `Riders are listed against ${chosenDistrict.name}.` : `${chosenDistrict.name} will be opened when this partner is saved.` : 'One district per partner. Choosing one Nearle does not run yet opens it.'} {/* ── Who they deliver for ────────────────────────────────────────── The merchant, then the branch. Both links are written: the merchant one is what the assign screen reads to decide whether to offer a partner tab at all, and the branch one records which outlet. */} Delivers for { // A new merchant clears the branch with it — keeping it would // leave another shop's outlet id attached to this partner. setForm((prev) => ({ ...prev, tenantid: Number(value) || 0, locationid: 0 })); }} options={merchantOptions} placeholder={merchants.isLoading ? 'Loading merchants…' : 'Choose a merchant'} /> set('locationid')(Number(value) || 0)} options={branchOptions} isDisabled={!form.tenantid} placeholder={ !form.tenantid ? 'Choose a merchant first' : branches.isLoading ? 'Loading branches…' : 'Choose the branch they cover' } /> Optional. Set it and this partner’s riders become an option on that shop’s assign screen, beside any riders it hired itself. ); }