import { useEffect, useState, useRef, useMemo, Fragment } from 'react'; import MainCard from 'components/MainCard'; import Loader from 'components/Loader'; import dayjs from 'dayjs'; import { enqueueSnackbar } from 'notistack'; import AddressAutocomplete from 'components/nearle_components/AddressAutocomplete'; import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard'; import { FaRegCheckCircle } from 'react-icons/fa'; import { TbBrandDatabricks, TbWorldLongitude, TbWorldLatitude } from 'react-icons/tb'; import { GiMoneyStack, GiModernCity } from 'react-icons/gi'; import { FiUser, FiPhoneCall } from 'react-icons/fi'; import { MdNumbers, MdGroups, MdCheckCircle, MdHourglassEmpty, MdCancel, MdOutlineCheckCircle, MdOutlinePendingActions, MdOutlineCancel, MdPersonPin, MdPlace, MdAdd, MdEdit, MdDeleteOutline, MdPeopleAlt, MdArrowUpward, MdArrowDownward, MdUnfoldMore, // Outline-only set for the Actions column — kept separate from the filled // icons above (MdPlace/MdEdit/MdPeopleAlt), which stay filled for the // Locations/Customers/Pricing dialogs elsewhere in this file. Mixing // filled and outlined glyphs in one row is what read as inconsistent. MdOutlineBlock, MdOutlineVisibility, MdOutlineVisibilityOff, MdOutlineEdit, MdOutlineClose, MdOutlineMoreVert, MdOutlinePlace, MdOutlinePeopleAlt } from 'react-icons/md'; import { LuMail } from 'react-icons/lu'; import { BiUser } from 'react-icons/bi'; // TanStack Table v9 — same engine orders.js/riders.js use, so sorting/ // resizing/pagination behave identically across the console's list pages. import { useTable, flexRender, tableFeatures, rowSortingFeature, createSortedRowModel, sortFns, columnSizingFeature, columnResizingFeature, rowPaginationFeature, createPaginatedRowModel } from '@tanstack/react-table'; import { Grid } from '@astryxdesign/core/Grid'; import { HStack } from '@astryxdesign/core/HStack'; import { VStack } from '@astryxdesign/core/VStack'; import { Card } from '@astryxdesign/core/Card'; import { Center } from '@astryxdesign/core/Center'; import { Text } from '@astryxdesign/core/Text'; import { Button } from '@astryxdesign/core/Button'; import { IconButton } from '@astryxdesign/core/IconButton'; import { Badge } from '@astryxdesign/core/Badge'; import { Divider } from '@astryxdesign/core/Divider'; import { TextInput } from '@astryxdesign/core/TextInput'; import { NumberInput } from '@astryxdesign/core/NumberInput'; import { Selector } from '@astryxdesign/core/Selector'; import { TabList, Tab } from '@astryxdesign/core/TabList'; import { Pagination } from '@astryxdesign/core/Pagination'; import { DropdownMenu } from '@astryxdesign/core/DropdownMenu'; import { Dialog, DialogHeader } from '@astryxdesign/core/Dialog'; import { Layout, LayoutContent, LayoutFooter } from '@astryxdesign/core/Layout'; import { EmptyState } from '@astryxdesign/core/EmptyState'; import { Table, TableHeader, TableBody, TableRow, TableCell, TableHeaderCell } from '@astryxdesign/core/Table'; import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete'; import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar'; import PageHeader from 'components/nearle_components/PageHeader'; import StatCard from 'components/nearle_components/StatCard'; import TableLoader from 'components/nearle_components/TableLoader'; import { PageShell, KpiGrid, DataCard } from 'components/nearle_components/PageLayout'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { getalltenants } from 'pages/api/api'; import { getAdminPricing, getDoormilePricing, createAdminPricing, updateAdminTenant, getTenantLocations, createTenantLocation, updateTenantLocation, getTenantCustomers, createTenantCustomer, updateTenantCustomer, deleteTenantCustomer } from 'pages/api/doormileApi'; import { useIsNarrowViewport } from 'hooks/useIsNarrowViewport'; import { DT } from 'themes/dt/tokens'; import { TableScroll, AccentAvatar } from 'themes/dt/primitives'; const BRAND = DT.brand; // Status palette — drives the pill tabs and per-row badges. Tenant status is // a business-entity state (active/pending approval/inactive), a different // domain from the order-lifecycle registry in themes/dt/status.js, so it // stays local here the same way riders.js's rider-presence STATUS_META does. const STATUS_META = { active: { label: 'Active', color: '#10b981', icon: MdCheckCircle, badge: 'green' }, pending: { label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty, badge: 'yellow' }, inactive: { label: 'Inactive', color: '#ef4444', icon: MdCancel, badge: 'red' } }; const STATUS_TABS = [ { status: 'active', countKey: 'active', tabStatus: 'Active' }, { status: 'pending', countKey: 'pending', tabStatus: 'Pending' }, { status: 'inactive', countKey: 'inactive', tabStatus: 'InActive' } ]; const KPI_META = [ { key: 'total', label: 'Total Tenants', color: BRAND, icon: MdGroups, countKey: 'total' }, { key: 'active', label: 'Active Tenants', color: '#10b981', icon: MdOutlineCheckCircle, countKey: 'active' }, { key: 'pending', label: 'Pending Approval', color: '#f59e0b', icon: MdOutlinePendingActions, countKey: 'pending' }, { key: 'inactive', label: 'Inactive Tenants', color: '#ef4444', icon: MdOutlineCancel, countKey: 'inactive' } ]; const VEHICLE_TYPE_OPTIONS = ['Bike', 'Scooter', 'Bicycle', 'Car', 'Van'].map((v) => ({ value: v, label: v })); // TanStack Table v9 registers features explicitly — only sorting/resizing/ // pagination ship here (no row-selection or column-visibility, since this // table has no bulk-select or column-hide flow). const TENANTS_TABLE_FEATURES = tableFeatures({ rowSortingFeature, sortedRowModel: createSortedRowModel(), sortFns, columnSizingFeature, columnResizingFeature, rowPaginationFeature, paginatedRowModel: createPaginatedRowModel() }); // Uppercase micro-label for the table's column headings — same convention // orders.js's and riders.js's tables use. const SectionLabel = ({ children }) => ( {children} ); // ==============================|| Tenant locations (branches/sites) dialog ||============================== // // Self-contained — deliberately does not touch the existing openRowIndex1/2 + // Tabs collapse machinery above, which is fragile legacy state. GET/POST/PUT // /admin/tenants/:id/locations + /admin/tenantlocations/:id per // jupiter2doormile.md §2.4 — no location UI existed anywhere before this. const emptyLocationForm = { locationid: null, locationname: '', address: '', city: '', state: '', pincode: '', isprimary: false, status: 'active' }; const TenantLocationsDialog = ({ tenant, open, onClose }) => { const queryClient = useQueryClient(); const [form, setForm] = useState(emptyLocationForm); const [formOpen, setFormOpen] = useState(false); const { data: locations = [], isLoading } = useQuery({ queryKey: ['tenant-locations', tenant?.tenantid], queryFn: () => getTenantLocations(tenant.tenantid), enabled: !!tenant?.tenantid && open }); const saveMutation = useMutation({ mutationFn: (payload) => payload.locationid ? updateTenantLocation(payload.locationid, payload.data) : createTenantLocation(tenant.tenantid, payload.data), onSuccess: (res) => { if (res.success) { enqueueSnackbar(form.locationid ? 'Location updated' : 'Location added', { variant: 'success', anchorOrigin: { vertical: 'top', horizontal: 'right' }, autoHideDuration: 2000 }); queryClient.invalidateQueries({ queryKey: ['tenant-locations', tenant?.tenantid] }); setForm(emptyLocationForm); setFormOpen(false); } else { enqueueSnackbar(res.message || 'Failed to save location', { variant: 'error', anchorOrigin: { vertical: 'top', horizontal: 'right' }, autoHideDuration: 2000 }); } }, onError: (err) => { enqueueSnackbar(err.response?.data?.message || err.message || 'Failed to save location', { variant: 'error', anchorOrigin: { vertical: 'top', horizontal: 'right' }, autoHideDuration: 2000 }); } }); const handleSave = () => { if (!form.locationname || !form.address) { enqueueSnackbar('Fill Location Name and Address', { variant: 'error', anchorOrigin: { vertical: 'top', horizontal: 'right' }, autoHideDuration: 2000 }); return; } const { locationid, ...data } = form; saveMutation.mutate({ locationid, data }); }; const openEdit = (loc) => { setForm({ locationid: loc.locationid, locationname: loc.locationname || '', address: loc.address || '', city: loc.city || '', state: loc.state || '', pincode: loc.pincode || '', isprimary: !!loc.isprimary, status: loc.status || 'active' }); setFormOpen(true); }; return ( !next && onClose()} width={560} purpose="form"> !next && onClose()} />} content={ {isLoading ? ( Loading locations… ) : locations.length === 0 && !formOpen ? ( } /> ) : ( {locations.map((loc) => ( {loc.locationname} {loc.isprimary && '· Primary'} {[loc.address, loc.city, loc.state, loc.pincode].filter(Boolean).join(', ')} } onClick={() => openEdit(loc)} /> ))} )} {formOpen && ( setForm((f) => ({ ...f, locationname: v }))} /> setForm((f) => ({ ...f, pincode: v }))} /> setForm((f) => ({ ...f, address: v }))} /> setForm((f) => ({ ...f, city: v }))} /> setForm((f) => ({ ...f, state: v }))} /> )} } footer={ {formOpen ? ( ); }; // ==============================|| Tenant customers dialog ||============================== // // GET /admin/tenantcustomers has no documented tenantid filter (unlike // locations, which are nested under /admin/tenants/:id/locations) — filtered // client-side here the same way fetchclientpricelist already filters // /admin/pricing client-side, since neither endpoint accepts the param. // createclient.js used to (incorrectly) create tenant customers instead of // tenants; now that it correctly creates tenants, this dialog is the only // place left that can create one. const emptyCustomerForm = { id: null, firstname: '', lastname: '', phone: '', email: '' }; // GET /admin/tenantcustomers doesn't document its id field name either — same // defensive fallback as customers.js's custId() rather than assuming `id`. const tenantCustId = (row) => row.id ?? row.tenantcustomerid ?? row.customerid; const TenantCustomersDialog = ({ tenant, open, onClose }) => { const queryClient = useQueryClient(); const [form, setForm] = useState(emptyCustomerForm); const [formOpen, setFormOpen] = useState(false); const { data: allCustomers = [], isLoading } = useQuery({ queryKey: ['tenant-customers-all'], queryFn: getTenantCustomers, enabled: open }); const customers = (allCustomers || []).filter((c) => !tenant?.tenantid || c.tenantid === tenant.tenantid); const saveMutation = useMutation({ mutationFn: (payload) => (payload.id ? updateTenantCustomer(payload.id, payload.data) : createTenantCustomer(payload.data)), onSuccess: (res) => { if (res.success) { enqueueSnackbar(form.id ? 'Customer updated' : 'Customer added', { variant: 'success', anchorOrigin: { vertical: 'top', horizontal: 'right' }, autoHideDuration: 2000 }); queryClient.invalidateQueries({ queryKey: ['tenant-customers-all'] }); setForm(emptyCustomerForm); setFormOpen(false); } else { enqueueSnackbar(res.message || 'Failed to save customer', { variant: 'error', anchorOrigin: { vertical: 'top', horizontal: 'right' }, autoHideDuration: 2000 }); } }, onError: (err) => { enqueueSnackbar(err.response?.data?.message || err.message || 'Failed to save customer', { variant: 'error', anchorOrigin: { vertical: 'top', horizontal: 'right' }, autoHideDuration: 2000 }); } }); const deleteMutation = useMutation({ mutationFn: (id) => deleteTenantCustomer(id), onSuccess: (res) => { if (res.success) { enqueueSnackbar('Customer deleted', { variant: 'success', anchorOrigin: { vertical: 'top', horizontal: 'right' }, autoHideDuration: 2000 }); queryClient.invalidateQueries({ queryKey: ['tenant-customers-all'] }); } else { enqueueSnackbar(res.message || 'Failed to delete customer', { variant: 'error', anchorOrigin: { vertical: 'top', horizontal: 'right' }, autoHideDuration: 2000 }); } }, onError: (err) => { enqueueSnackbar(err.response?.data?.message || err.message || 'Failed to delete customer', { variant: 'error', anchorOrigin: { vertical: 'top', horizontal: 'right' }, autoHideDuration: 2000 }); } }); const handleSave = () => { if (!form.firstname || !form.phone) { enqueueSnackbar('Fill Name and Phone', { variant: 'error', anchorOrigin: { vertical: 'top', horizontal: 'right' }, autoHideDuration: 2000 }); return; } const { id, ...data } = form; saveMutation.mutate({ id, data }); }; const openEdit = (row) => { setForm({ id: tenantCustId(row), firstname: row.firstname || '', lastname: row.lastname || '', // GET/POST /admin/tenantcustomers has a write/read asymmetry confirmed // live: POST accepts `phone`, but the stored/returned field is // `contactno`. The form field itself stays named `phone` (matching // what create/update accept) — only the read source differs. phone: row.contactno || '', email: row.email || '' }); setFormOpen(true); }; const handleDelete = (row) => { if (window.confirm(`Delete customer "${row.firstname}"?`)) deleteMutation.mutate(tenantCustId(row)); }; return ( !next && onClose()} width={560} purpose="form"> !next && onClose()} />} content={ {isLoading ? ( Loading customers… ) : customers.length === 0 && !formOpen ? ( } /> ) : ( {customers.map((c) => ( {c.firstname} {c.lastname} {[c.contactno, c.email].filter(Boolean).join(' · ')} } onClick={() => openEdit(c)} /> } onClick={() => handleDelete(c)} /> ))} )} {formOpen && ( setForm((f) => ({ ...f, firstname: v }))} /> setForm((f) => ({ ...f, lastname: v }))} /> setForm((f) => ({ ...f, phone: v }))} /> setForm((f) => ({ ...f, email: v }))} /> )} } footer={ {formOpen ? ( ); }; // A single icon + value + caption row used throughout the tenant detail/edit panels. const InfoRow = ({ icon: Icon, value, caption }) => ( {value || '—'} {caption} ); const PricingTable = ({ list }) => ( # Vehicle Base Price Price/Km Price/Kg Handling Status {list?.length === 0 ? ( } /> ) : ( list.map((val, i) => ( {i + 1} {val.vehicletype || '—'} {val.baseprice ?? '—'} {val.priceperkm ?? '—'} {val.priceperkg ?? '—'} {val.handlingcharges ?? '—'} {val.status || '—'} )) )}
); // ==============================|| Starts here||============================== // const Clients1 = () => { const textFieldRef = useRef(null); const [searchword, setSearchword] = useState(''); const [debouncedSearch, setDebouncedSearch] = useState(''); const [rowsPerPage, setRowsPerPage] = useState(10); const isMobile = useIsNarrowViewport(); const [isloader, setisloader] = useState(false); const [appId, setAppId] = useState(0); const [locaName, setLocoName] = useState(''); const [value0, setValue0] = useState(0); const [value1, setValue1] = useState('0'); const [value2, setValue2] = useState('0'); const [selectedTenid, setSelectedtenid] = useState(null); const [clientpricelist, setClientpricelist] = useState([]); const [status, setstatus] = useState('active'); const [selectedCustomer, setSelectedCustomer] = useState({}); // to edit const [dialogopen, setDialogopen] = useState(false); const [, setAppPricing] = useState([]); const [selectedPricing, setSelectedPricing] = useState({}); const [isPrice, setIsprice] = useState(true); const [city, setCity] = useState(''); const [zipcode, setZipcode] = useState(''); const [latlong, setLatlong] = useState({}); const [editClient, setEditClient] = useState({}); const [page, setPage] = useState(0); // Table sorting/resizing state — mirrors orders.js's/riders.js's slices. // Pagination itself stays on the existing page/rowsPerPage state (below) // rather than a new `pagination` object, since page/rowsPerPage already // feed the tenantList query key and several onClick handlers — bridged // into TanStack's { pageIndex, pageSize } shape via paginationState/ // handlePaginationChange further down. const [sorting, setSorting] = useState([]); const [columnSizing, setColumnSizing] = useState({}); const dialogclose = () => { setDialogopen(false); }; const handleChange = (newValue) => { setValue0(newValue); setSearchword(''); setPage(0); }; const [openRowIndex1, setOpenRowIndex1] = useState(null); // Initially no row is open for collapsible section 1 const [openRowIndex2, setOpenRowIndex2] = useState(null); // Initially no row is open for collapsible section 2 const [locationsDialogTenant, setLocationsDialogTenant] = useState(null); const [customersDialogTenant, setCustomersDialogTenant] = useState(null); // Handle toggling for collapsible section 1 const handleCollapseToggle1 = (rowIndex) => { setOpenRowIndex1((prevIndex) => (prevIndex === rowIndex ? null : rowIndex)); }; // Handle toggling for collapsible section 2 const handleCollapseToggle2 = (rowIndex) => { setOpenRowIndex2((prevIndex) => (prevIndex === rowIndex ? null : rowIndex)); }; useEffect(() => { setOpenRowIndex1(-1); setOpenRowIndex2(-1); }, [value0]); /* ============================================= || opentoast || ============================================= */ const opentoast = (message, variant, duration) => { enqueueSnackbar(message, { variant: variant, anchorOrigin: { vertical: 'top', horizontal: 'right' }, autoHideDuration: duration }); }; // ==============================|| address autocomplete ||============================== // const handleAddressPlaceSelected = (place) => { const lat = place.geometry.location.lat(); const lng = place.geometry.location.lng(); setLatlong({ lat, lng }); setEditClient((prev) => ({ ...prev, address: place.formatted_address, latitude: lat.toString(), longitude: lng.toString() })); let city1, zipcode1, state1, suburb1; place.address_components.forEach((component) => { component.types.forEach((type) => { switch (type) { case 'locality': city1 = component.long_name; break; case 'administrative_area_level_1': state1 = component.long_name; break; case 'postal_code': zipcode1 = component.long_name; break; case 'sublocality': case 'sublocality_level_1': suburb1 = component.long_name; break; } }); }); setCity(city1 || ''); setState(state1 || ''); setZipcode(zipcode1 || ''); setSuburb(suburb1 || ''); setEditClient((prev) => ({ ...prev, city: city1 || '', state: state1 || '', postcode: zipcode1 || '', suburb: suburb1 || '' })); }; useEffect(() => { selectedCustomer && setLatlong({ lat: selectedCustomer.latitude, lng: selectedCustomer.longitude }); }, [selectedCustomer]); /* ============================================= || handleKeyPress (ctrl+k)| ============================================= */ useEffect(() => { const handleKeyPress = (event) => { if (event.key === 'k' && (event.metaKey || event.ctrlKey)) { event.preventDefault(); textFieldRef.current.focus(); } if (event.key === 'Escape' && document.activeElement === textFieldRef.current) { // Remove focus from the TextField textFieldRef.current.blur(); } }; document.addEventListener('keydown', handleKeyPress); return () => { document.removeEventListener('keydown', handleKeyPress); }; }, []); /* ============================================= || getalltenants| ============================================= */ const { data: tenantList, isLoading: getalltenantsIsLoading, refetch: getalltenantsRefetch } = useQuery({ queryKey: ['tenantList', appId, debouncedSearch, status, page, rowsPerPage], queryFn: getalltenants }); // No tenant-tab-counts endpoint in the new API (gettenantsummary always // returns null) — derive the Active/Pending/Inactive counts client-side // from the full tenant list instead, which getalltenants already fetches. const tenantStatusCounts = useMemo(() => { const counts = { active: 0, pending: 0, inactive: 0 }; (tenantList || []).forEach((t) => { const s = String(t.status || '').toLowerCase(); if (s === 'active') counts.active += 1; else if (s === 'pending') counts.pending += 1; else if (s === 'inactive') counts.inactive += 1; }); return { ...counts, total: (tenantList || []).length }; }, [tenantList]); // getalltenants() takes no filter args (see api.js) — the status tabs and // search box only ever changed the query-cache key, never the actual // request, so every tab showed the exact same unfiltered list with just a // relabeled status badge. Filter the already-fetched list client-side // instead, matching the same real `status` field tenantStatusCounts uses. const filteredTenantList = useMemo(() => { const activeStatus = STATUS_TABS[value0]?.status; const q = debouncedSearch.trim().toLowerCase(); return (tenantList || []).filter((t) => { if (String(t.status || '').toLowerCase() !== activeStatus) return false; if (!q) return true; return ( t.tenantname?.toLowerCase().includes(q) || t.primaryemail?.toLowerCase().includes(q) || t.primarycontact?.includes(debouncedSearch) ); }); }, [tenantList, value0, debouncedSearch]); /* ============================================= || fetchclientpricelist || ============================================= */ // /admin/pricing has no tenantid query param documented — filter client-side. const fetchclientpricelist = async () => { try { const pricing = await getAdminPricing(); setClientpricelist((pricing || []).filter((p) => p.tenantid === selectedTenid)); } catch (err) { // swallow } }; useEffect(() => { selectedTenid && fetchclientpricelist(); }, [selectedTenid]); /* ============================================= || fetchTenanatPricing || ============================================= */ const fetchTenanatPricing = async (id) => { try { const pricing = await getAdminPricing(); setTenanatPricing((pricing || []).filter((p) => p.tenantid === id)); } catch (error) { // swallow } }; useEffect(() => { selectedTenid && fetchTenanatPricing(selectedTenid); }, [selectedTenid]); /* ============================================= || tenantupdate || ============================================= */ const tenantupdate = async (tenid) => { setisloader(true); let targetId; let updateData; if (tenid == -1) { targetId = selectedTenid; updateData = { approved: 1 }; } else { targetId = tenid; updateData = { status: status === 'active' ? 'InActive' : 'Active' }; } try { const res = await updateAdminTenant(targetId, updateData); if (res.success) { opentoast( value0 == 0 ? 'Inactivated Successfully' : value0 == 1 ? 'Approved Successfully' : 'Activate Successfully', 'success', 2000 ); getalltenantsRefetch(); } } catch (err) { opentoast(err.message, 'error', 1500); } finally { setisloader(false); } }; /* ============================================= || getAppPricing || ============================================= */ // /admin/doormile-pricing has no zone query param documented — filter client-side. const getAppPricing = async (id) => { try { const pricing = await getDoormilePricing(); setAppPricing((pricing || []).filter((p) => !id || p.applocationid === id)); } catch (error) { // swallow } }; /* ============================================= || createpricing || ============================================= */ const createpricing = async () => { setisloader(true); try { const res = await createAdminPricing({ applocationid: appId, tenantid: selectedTenid, vehicletype: selectedPricing.vehicletype || 'Bike', baseprice: +selectedPricing.baseprice || 0, baseweight: +selectedPricing.baseweight || 0, priceperkm: +selectedPricing.priceperkm || 0, priceperkg: +selectedPricing.priceperkg || 0, basedistance: +selectedPricing.basedistance || 0, handlingcharges: +selectedPricing.handlingcharges || 0, // Confirmed live: POST /admin/pricing rejects a bare YYYY-MM-DD date // for effectivefrom ("invalid request body") — needs a full ISO // datetime. Status also confirmed to need title case ("Active"), // matching every real pricing row. effectivefrom: dayjs().toISOString(), currency: 'INR', priority: 1, status: 'Active' }); if (res.success) { enqueueSnackbar('Price Created Successfully', { variant: 'success', anchorOrigin: { vertical: 'top', horizontal: 'right' }, autoHideDuration: 2000 }); dialogclose(); setSelectedPricing({}); tenantupdate(-1); fetchclientpricelist(); } else { opentoast(res.message || 'Failed to save pricing', 'error', 2000); } } catch (err) { opentoast(err.response?.data?.message || err.message || 'Failed to save pricing', 'error', 2000); } }; /* ============================================= || updateClient || ============================================= */ const updateClient = async () => { try { const updateRes = await updateAdminTenant(selectedTenid, editClient); if (updateRes.success) { opentoast(updateRes.message || 'Updated Successfully', 'success', 1500); } else { opentoast(updateRes.message || 'Update Failed', 'error', 1500); } getalltenantsRefetch(); } catch (err) { opentoast(err.response?.data?.message || err.message || 'Update Failed', 'error', 1500); } }; const activeTabMeta = STATUS_META[STATUS_TABS[value0]?.status] || STATUS_META.active; // Icon buttons only, kept to 3 max + one overflow menu: status action, // view details, edit, then "More" (Locations/Customers — secondary, // low-frequency actions) behind a kebab menu. // // Sizing: `size="md"` is Astryx's own 32px button height (confirmed in // Button's sizeStyles — sm=28px/md=32px/lg=36px) and icon-only buttons are // always square, so this is a native 32×32px button, not a manually forced // one. Its border-radius is Astryx's own default --radius-element (8px — // see the identical note in dispatch/Dispatch.css), so no radius override // is needed either — both requirements were already the design system's // own defaults once the size prop was corrected. // // Icon family: every icon here is the Material "outline" variant // (MdOutline*). Mixing MdBlock/MdVisibility/MdEdit/MdClose/MdMoreVert // (filled) with MdOutlineCheckCircle (outline) — the state before this // pass — is what actually read as inconsistent, not the icons' size or // position. MdPlace/MdEdit/MdPeopleAlt stay filled where they're used // elsewhere in this file (Locations/Customers/Pricing dialogs) — only this // row switches to the Outline set. // // Colour: the status-action slot (Inactivate/Approve/Activate — whichever // one applies to the active tab) is the one action with real consequence // for the tenant's account state, so it keeps the `destructive` (red- // tinted) treatment across all three tab variants for a consistent, // predictable "this is the serious one" signal. View/Edit/More stay // `ghost` (transparent, neutral) — `primary` only kicks in on View/Edit // when their panel is the one currently open, an existing state indicator // preserved as-is, not a new style. const renderRowActions = (row, index, expanded, size = 'md') => ( {value0 == 0 && ( } onClick={() => { setSelectedCustomer(row); setSelectedtenid(row.tenantid); setAppId(row.applocationid); setTimeout(() => { tenantupdate(row.tenantid); }, 100); }} /> )} {value0 == 1 && ( } onClick={() => { setSelectedCustomer(row); setDialogopen(true); setSelectedtenid(row.tenantid); setAppId(row.applocationid); getAppPricing(row.applocationid); }} /> )} {value0 == 2 && ( } onClick={() => { setSelectedCustomer(row); setSelectedtenid(row.tenantid); setAppId(row.applocationid); setTimeout(() => { tenantupdate(row.tenantid); }, 100); }} /> )} : } onClick={() => { setSelectedCustomer(row); handleCollapseToggle1(index); setOpenRowIndex2(-1); setSelectedtenid(row.tenantid); }} /> {value0 !== 1 && ( : } onClick={() => { setSelectedCustomer(row); handleCollapseToggle2(index); setOpenRowIndex1(-1); setSelectedtenid(row.tenantid); setAppId(row.applocationid); }} /> )} , isIconOnly: true }} menuWidth={180} placement="below" items={[ { label: 'Locations', icon: , onClick: () => setLocationsDialogTenant(row) }, { label: 'Customers', icon: , onClick: () => setCustomersDialogTenant(row) } ]} /> ); const renderDetailsPanel = (row) => ( } /> } /> } > {value1 == '0' && ( )} {value1 == '1' && ( )} ); const renderEditPanel = (row) => ( { setValue2(v); if (v === '1') getAppPricing(selectedCustomer.applocationid); }} size="sm" > } /> {value0 !== 1 && } />} {value2 == '1' && (