rider partner page
This commit is contained in:
612
src/features/nearle-admin/pages/PartnersPage.tsx
Normal file
612
src/features/nearle-admin/pages/PartnersPage.tsx
Normal file
@@ -0,0 +1,612 @@
|
||||
/**
|
||||
* 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<string, unknown> {
|
||||
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<Partner | 'new' | null>(null);
|
||||
/** The partner whose riders are on screen, if any. */
|
||||
const [ridersFor, setRidersFor] = useState<Partner | null>(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<number, string>();
|
||||
for (const region of regions.data ?? []) {
|
||||
map.set(region.applocationid, region.locationname ?? `Region ${region.applocationid}`);
|
||||
}
|
||||
return map;
|
||||
}, [regions.data]);
|
||||
|
||||
const rows = useMemo<PartnerRow[]>(
|
||||
() =>
|
||||
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<PartnerRow>[] = [
|
||||
{
|
||||
key: 'partnername',
|
||||
header: 'Partner',
|
||||
width: { type: 'proportional', value: 3 },
|
||||
renderCell: (row) => (
|
||||
<VStack gap={0}>
|
||||
<Text type="label" size="sm" weight="semibold">
|
||||
{row.partnername}
|
||||
</Text>
|
||||
{row.companyname ? (
|
||||
<Text type="body" size="xsm" color="secondary">
|
||||
{row.companyname}
|
||||
</Text>
|
||||
) : null}
|
||||
</VStack>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'region',
|
||||
header: 'Home region',
|
||||
width: { type: 'proportional', value: 2 },
|
||||
renderCell: (row) => (
|
||||
<Text type="body" size="sm" color="secondary">
|
||||
{row.region}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'contact',
|
||||
header: 'Contact',
|
||||
width: { type: 'proportional', value: 2 },
|
||||
renderCell: (row) => (
|
||||
<Text type="body" size="sm" style={{ fontFamily: 'var(--font-mono)' }}>
|
||||
{row.contact || '—'}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
header: 'Status',
|
||||
align: 'end',
|
||||
width: { type: 'pixel', value: 110 },
|
||||
renderCell: (row) => (
|
||||
<Badge
|
||||
variant={row.status.toLowerCase() === 'active' ? 'success' : 'neutral'}
|
||||
label={row.status}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
/* 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) => (
|
||||
<HStack gap={0.5} justify="end" align="center" className="fleet-actions">
|
||||
<Button
|
||||
/* One width for every row, fixed in CSS. The label is a count, so
|
||||
it runs from one digit to three and a shrink-to-fit button leaves
|
||||
the column ragged — an uneven edge reads as disorder before the
|
||||
numbers themselves are read. */
|
||||
label={row.riders === null ? '—' : `${row.riders} rider${row.riders === 1 ? '' : 's'}`}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
icon={<Bike size={13} />}
|
||||
onClick={() => {
|
||||
const found = partners.data.find((p) => p.partnerid === row.partnerid);
|
||||
if (found) setRidersFor(found);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
label="Edit"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const found = partners.data.find((p) => p.partnerid === row.partnerid);
|
||||
if (found) setEditing(found);
|
||||
}}
|
||||
/>
|
||||
</HStack>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<VStack gap={3}>
|
||||
<PageHeader
|
||||
title="Rider partners"
|
||||
actions={
|
||||
<Button
|
||||
label="Onboard rider partner"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
icon={<Plus size={14} />}
|
||||
onClick={() => setEditing('new')}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<VStack gap={1.5}>
|
||||
{/* No section heading. The page is already titled "Rider partners" and
|
||||
the table is the only thing on it — a second heading over one table
|
||||
restates the page and pushes the rows down a row for nothing. */}
|
||||
<Card padding={0} elevation="low">
|
||||
<DataState
|
||||
isLoading={partners.isLoading}
|
||||
error={null}
|
||||
isEmpty={rows.length === 0}
|
||||
emptyTitle="No rider partners yet"
|
||||
emptyDescription="A rider partner is the company that supplies riders. Onboard one, choose its district, and attach it to the shop and branch it delivers for."
|
||||
>
|
||||
<div className="table-scroll">
|
||||
<Table<PartnerRow>
|
||||
data={paged.rows}
|
||||
columns={columns}
|
||||
idKey="partnerid"
|
||||
density="balanced"
|
||||
hasHover
|
||||
dividers="rows"
|
||||
/>
|
||||
</div>
|
||||
<TablePager paged={paged} label="partners" />
|
||||
</DataState>
|
||||
</Card>
|
||||
</VStack>
|
||||
|
||||
{ridersFor ? (
|
||||
<PartnerRidersDrawer partner={ridersFor} onClose={() => setRidersFor(null)} />
|
||||
) : null}
|
||||
|
||||
{editing ? (
|
||||
<PartnerDrawer
|
||||
partner={editing === 'new' ? null : editing}
|
||||
onClose={() => setEditing(null)}
|
||||
/>
|
||||
) : null}
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── The form ─────────────────────────────────────────────────────────────── */
|
||||
|
||||
interface FormState {
|
||||
partnername: string;
|
||||
companyname: string;
|
||||
registrationno: string;
|
||||
primarycontact: string;
|
||||
primaryemail: string;
|
||||
address: string;
|
||||
suburb: string;
|
||||
city: string;
|
||||
state: string;
|
||||
postcode: string;
|
||||
/** The serviced district they work out of — an `app_location` id. */
|
||||
applocationid: number;
|
||||
/** The merchant this partner delivers for. */
|
||||
tenantid: number;
|
||||
/** Which branch of that merchant — written to `tenantlocations.partnerid`. */
|
||||
locationid: number;
|
||||
}
|
||||
|
||||
const EMPTY: FormState = {
|
||||
partnername: '',
|
||||
companyname: '',
|
||||
registrationno: '',
|
||||
primarycontact: '',
|
||||
primaryemail: '',
|
||||
address: '',
|
||||
suburb: '',
|
||||
city: '',
|
||||
state: '',
|
||||
postcode: '',
|
||||
applocationid: 0,
|
||||
tenantid: 0,
|
||||
locationid: 0,
|
||||
};
|
||||
|
||||
function PartnerDrawer({ partner, onClose }: { partner: Partner | null; onClose: () => void }) {
|
||||
const client = useQueryClient();
|
||||
const regions = useAppRegions();
|
||||
const isNew = partner === null;
|
||||
|
||||
const [form, setForm] = useState<FormState>(() =>
|
||||
partner
|
||||
? {
|
||||
partnername: partner.partnername ?? '',
|
||||
companyname: partner.companyname ?? '',
|
||||
registrationno: partner.registrationno ?? '',
|
||||
primarycontact: partner.primarycontact ?? partner.contactno ?? '',
|
||||
primaryemail: partner.primaryemail ?? '',
|
||||
address: partner.address ?? '',
|
||||
suburb: partner.suburb ?? '',
|
||||
city: partner.city ?? '',
|
||||
state: partner.state ?? '',
|
||||
postcode: '',
|
||||
applocationid: partner.applocationid ?? 0,
|
||||
tenantid: 0,
|
||||
locationid: 0,
|
||||
}
|
||||
: EMPTY,
|
||||
);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
function set<K extends keyof FormState>(key: K) {
|
||||
return (value: FormState[K]) => {
|
||||
setError(null);
|
||||
setForm((prev) => ({ ...prev, [key]: value }));
|
||||
};
|
||||
}
|
||||
|
||||
/* ── The district ─────────────────────────────────────────────────────────
|
||||
One per partner, chosen from all 38. Picking one Nearle does not run yet
|
||||
opens it — the partner is sent with the NAME and the server writes the
|
||||
`app_location` and `app_locationconfig` rows first. */
|
||||
const [districtSearch, setDistrictSearch] = useState('');
|
||||
const [district, setDistrict] = useState<string>(() => partner?.city ?? '');
|
||||
const districts = useMemo(() => districtOptions(regions.data ?? []), [regions.data]);
|
||||
const shown = useMemo(
|
||||
() => districts.filter((option) => matchDistrict(option, districtSearch)),
|
||||
[districts, districtSearch],
|
||||
);
|
||||
const chosenDistrict = districts.find((option) => option.name === district);
|
||||
|
||||
/* ── Who they deliver for ─────────────────────────────────────────────────
|
||||
A partner supplies riders TO a merchant's branch. Both links are written:
|
||||
`tenants.partnerid`, which is what the assign screen reads to decide
|
||||
whether to offer a partner tab at all, and `tenantlocations.partnerid`,
|
||||
which records the branch. Without the first the toggle never appears;
|
||||
without the second nothing says which outlet they cover.
|
||||
|
||||
Filtered to the district: a partner works one district, so a merchant in
|
||||
another is not somebody they can deliver for. `getalltenants` returns a row
|
||||
per BRANCH, and a branch's city is what places it — the tenant's own city
|
||||
is the head office and can differ. */
|
||||
const merchants = useTenants({ pageno: 1, pagesize: 200 });
|
||||
const merchantOptions = useMemo(() => {
|
||||
const here = district.trim().toLowerCase();
|
||||
const seen = new Map<number, string>();
|
||||
for (const entry of (merchants.data ?? []) as TenantInfo[]) {
|
||||
if (!entry.tenantid || seen.has(entry.tenantid)) continue;
|
||||
const city = (entry.city ?? '').trim().toLowerCase();
|
||||
if (here && city && city !== here) continue;
|
||||
seen.set(entry.tenantid, entry.tenantname);
|
||||
}
|
||||
return [...seen.entries()].map(([value, label]) => ({ value: String(value), label }));
|
||||
}, [merchants.data, district]);
|
||||
|
||||
const branches = useTenantLocations(form.tenantid || undefined);
|
||||
const branchOptions = useMemo(
|
||||
() =>
|
||||
(branches.data ?? []).map((branch) => ({
|
||||
value: String(branch.locationid),
|
||||
label: branch.locationname || `Branch ${branch.locationid}`,
|
||||
})),
|
||||
[branches.data],
|
||||
);
|
||||
|
||||
const save = useMutation({
|
||||
mutationFn: () => {
|
||||
const body: NewPartner = {
|
||||
partnername: form.partnername.trim(),
|
||||
companyname: form.companyname.trim(),
|
||||
registrationno: form.registrationno.trim(),
|
||||
primarycontact: form.primarycontact.trim(),
|
||||
primaryemail: form.primaryemail.trim(),
|
||||
address: form.address.trim(),
|
||||
suburb: form.suburb.trim(),
|
||||
city: form.city.trim(),
|
||||
state: form.state.trim(),
|
||||
...(form.postcode.trim() ? { postcode: Number(form.postcode) || 0 } : {}),
|
||||
/*
|
||||
The district, by id when Nearle already runs it and by NAME when it
|
||||
does not. The name is what opens it — the server writes the region
|
||||
rows before the partner, so all 38 are real choices rather than three.
|
||||
*/
|
||||
applocationid: chosenDistrict?.applocationid ?? 0,
|
||||
...(chosenDistrict && chosenDistrict.applocationid === 0
|
||||
? { district: chosenDistrict.name }
|
||||
: {}),
|
||||
};
|
||||
return partner
|
||||
? partnersApi.update({ ...body, partnerid: partner.partnerid }).then(() => partner.partnerid)
|
||||
: partnersApi.create(body).then((result) => result?.partnerid ?? 0);
|
||||
},
|
||||
/*
|
||||
The placement is written after the partner exists, because it needs the
|
||||
id the create hands back.
|
||||
|
||||
Reported separately if it fails, and deliberately not rolled back: the
|
||||
partner is real either way and re-onboarding them would refuse on the
|
||||
duplicate contact number. Saying "the partner was created but could not be
|
||||
placed" is recoverable — the drawer stays open on the same form.
|
||||
*/
|
||||
onSuccess: async (partnerid) => {
|
||||
if (partnerid > 0 && form.tenantid > 0) {
|
||||
try {
|
||||
await tenantsApi.assignPartner(form.tenantid, partnerid);
|
||||
if (form.locationid > 0) {
|
||||
await tenantsApi.updateBranch({ locationid: form.locationid, partnerid });
|
||||
}
|
||||
} catch (cause) {
|
||||
await client.invalidateQueries({ queryKey: queryKeys.partners.all });
|
||||
setError(
|
||||
`${form.partnername.trim()} was saved, but could not be assigned to that branch: ${errorMessage(cause)}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
await client.invalidateQueries({ queryKey: queryKeys.partners.all });
|
||||
await client.invalidateQueries({ queryKey: queryKeys.tenants.all });
|
||||
onClose();
|
||||
},
|
||||
onError: (cause) => setError(errorMessage(cause)),
|
||||
});
|
||||
|
||||
const isComplete =
|
||||
form.partnername.trim() !== '' && form.primarycontact.trim() !== '' && form.applocationid > 0;
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
title={isNew ? 'Onboard a rider partner' : form.partnername || 'Rider partner'}
|
||||
subtitle={isNew ? 'The company that supplies riders' : `Partner ${partner?.partnerid}`}
|
||||
width={520}
|
||||
onClose={onClose}
|
||||
isFooterSpread
|
||||
footer={
|
||||
<>
|
||||
<DrawerButton label="Cancel" variant="ghost" onClick={onClose} />
|
||||
<DrawerButton
|
||||
label={save.isPending ? 'Saving…' : isNew ? 'Onboard rider partner' : 'Save changes'}
|
||||
variant="primary"
|
||||
icon={<Truck size={15} />}
|
||||
isDisabled={!isComplete || save.isPending}
|
||||
onClick={() => save.mutate()}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<VStack gap={2}>
|
||||
{error ? (
|
||||
<Text type="body" size="sm" style={{ color: 'var(--color-error, #d64545)' }}>
|
||||
{error}
|
||||
</Text>
|
||||
) : null}
|
||||
|
||||
<TextInput
|
||||
label="Partner name"
|
||||
size="sm"
|
||||
value={form.partnername}
|
||||
onChange={set('partnername')}
|
||||
placeholder="e.g. Xpress-Cbe-Main"
|
||||
description="How the partner is named on rider records and in the assign picker."
|
||||
/>
|
||||
<TextInput
|
||||
label="Registered company"
|
||||
size="sm"
|
||||
value={form.companyname}
|
||||
onChange={set('companyname')}
|
||||
/>
|
||||
<TextInput
|
||||
label="Primary contact"
|
||||
size="sm"
|
||||
value={form.primarycontact}
|
||||
onChange={set('primarycontact')}
|
||||
placeholder="9876543210"
|
||||
description="One partner per number — the server refuses a second."
|
||||
/>
|
||||
<TextInput
|
||||
label="Email"
|
||||
size="sm"
|
||||
value={form.primaryemail}
|
||||
onChange={set('primaryemail')}
|
||||
/>
|
||||
<TextInput
|
||||
label="Registration number"
|
||||
size="sm"
|
||||
value={form.registrationno}
|
||||
onChange={set('registrationno')}
|
||||
/>
|
||||
|
||||
{/* ── District ──────────────────────────────────────────────────────
|
||||
All 38 of Tamil Nadu's districts, searchable, with only the ones
|
||||
Nearle services selectable. A partner placed in a district that has
|
||||
no `app_location` row is a partner whose riders no query returns —
|
||||
`getriders` filters on that id — so an unserviced district is shown
|
||||
and refused rather than hidden, because "Erode is not open yet" is
|
||||
an answer and a missing Erode is not. */}
|
||||
<VStack gap={1}>
|
||||
<Text type="label" size="sm" weight="semibold">
|
||||
District
|
||||
</Text>
|
||||
<TextInput
|
||||
label="Search districts"
|
||||
isLabelHidden
|
||||
size="sm"
|
||||
value={districtSearch}
|
||||
onChange={setDistrictSearch}
|
||||
placeholder="Search all 38 districts…"
|
||||
hasClear
|
||||
/>
|
||||
{/* A single-select list, not a cloud of chips: one partner works one
|
||||
district, so this is a choice with one answer and it should read
|
||||
like one. Running districts carry a tick, new ones say what will
|
||||
happen — the difference is operational, not a restriction. */}
|
||||
<div className="district-list" role="listbox" aria-label="Tamil Nadu districts">
|
||||
{shown.map((option) => {
|
||||
const running = isRunning(option);
|
||||
const chosen = district === option.name;
|
||||
return (
|
||||
<button
|
||||
key={option.name}
|
||||
type="button"
|
||||
role="option"
|
||||
aria-selected={chosen}
|
||||
className="district-row"
|
||||
data-chosen={chosen ? 'yes' : 'no'}
|
||||
onClick={() => {
|
||||
setDistrict(option.name);
|
||||
// The merchant is district-scoped, so changing the district
|
||||
// invalidates it — keeping it would attach a partner to a
|
||||
// shop in a place they do not work.
|
||||
setForm((prev) => ({ ...prev, tenantid: 0, locationid: 0 }));
|
||||
}}
|
||||
>
|
||||
<span className="district-row-name">{option.name}</span>
|
||||
<span className="district-row-state" data-running={running ? 'yes' : 'no'}>
|
||||
{running ? 'Running' : 'New district'}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{shown.length === 0 ? (
|
||||
<Text type="body" size="sm" color="secondary" style={{ padding: '10px 12px' }}>
|
||||
No district matches “{districtSearch}”.
|
||||
</Text>
|
||||
) : null}
|
||||
</div>
|
||||
<Text type="body" size="xsm" color="secondary">
|
||||
{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.'}
|
||||
</Text>
|
||||
</VStack>
|
||||
|
||||
{/* ── 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. */}
|
||||
<VStack gap={1}>
|
||||
<Text type="label" size="sm" weight="semibold">
|
||||
Delivers for
|
||||
</Text>
|
||||
<Selector
|
||||
label="Merchant"
|
||||
size="sm"
|
||||
value={form.tenantid ? String(form.tenantid) : ''}
|
||||
onChange={(value) => {
|
||||
// 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'}
|
||||
/>
|
||||
<Selector
|
||||
label="Branch"
|
||||
size="sm"
|
||||
value={form.locationid ? String(form.locationid) : ''}
|
||||
onChange={(value) => 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'
|
||||
}
|
||||
/>
|
||||
<Text type="body" size="xsm" color="secondary">
|
||||
Optional. Set it and this partner’s riders become an option on that shop’s assign
|
||||
screen, beside any riders it hired itself.
|
||||
</Text>
|
||||
</VStack>
|
||||
|
||||
<TextInput label="Address" size="sm" value={form.address} onChange={set('address')} />
|
||||
<HStack gap={1}>
|
||||
<TextInput label="Area" size="sm" value={form.suburb} onChange={set('suburb')} />
|
||||
<TextInput label="City" size="sm" value={form.city} onChange={set('city')} />
|
||||
</HStack>
|
||||
<HStack gap={1}>
|
||||
<TextInput label="State" size="sm" value={form.state} onChange={set('state')} />
|
||||
<TextInput label="Postcode" size="sm" value={form.postcode} onChange={set('postcode')} />
|
||||
</HStack>
|
||||
</VStack>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user