{isFetchingNextPage || hasNextPage ? (
diff --git a/src/pages/nearle/dashboard/Dashboard.js b/src/pages/nearle/dashboard/Dashboard.js
index 9a1f00d..35d45c7 100644
--- a/src/pages/nearle/dashboard/Dashboard.js
+++ b/src/pages/nearle/dashboard/Dashboard.js
@@ -9,7 +9,7 @@ import {
MdOutlineCheckCircle,
MdTwoWheeler,
MdOutlineSmartToy,
- MdDeploymentUnit,
+ MdLocationCity,
MdArrowForward,
MdCircle
} from 'react-icons/md';
@@ -135,7 +135,7 @@ const Dashboard = () => {
-
+
City Breakdown
@@ -297,7 +297,7 @@ const Dashboard = () => {
>
-
+
Manage Hubs
diff --git a/src/pages/nearle/hubs/Hubs.js b/src/pages/nearle/hubs/Hubs.js
index 890f416..10cfeb1 100644
--- a/src/pages/nearle/hubs/Hubs.js
+++ b/src/pages/nearle/hubs/Hubs.js
@@ -1,4 +1,5 @@
import { useState, Fragment } from 'react';
+import axios from 'axios';
import {
Avatar,
Box,
@@ -23,31 +24,17 @@ import {
TableRow,
TextField,
Tooltip,
- Typography,
- useMediaQuery
+ Typography
} from '@mui/material';
-import { useTheme } from '@mui/material/styles';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
-import {
- MdDeploymentUnit,
- MdOutlineWarehouse,
- MdOutlineCheckCircle,
- MdOutlineBarChart,
- MdOutlineLocationCity,
- MdAdd,
- MdEdit,
- MdKeyboardArrowDown,
- MdKeyboardArrowUp,
- MdTwoWheeler
-} from 'react-icons/md';
-import StatCard from 'components/nearle_components/StatCard';
+import { DeploymentUnitOutlined } from '@ant-design/icons';
+import { MdAdd, MdEdit, MdKeyboardArrowDown, MdKeyboardArrowUp, MdOutlineWarehouse, MdTwoWheeler, MdStar } from 'react-icons/md';
import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton';
import { OpenToast } from 'components/third-party/OpenToast';
-import { fetchHubs, createHub, updateHub, fetchAllRiders } from 'pages/api/api';
+import { fetchHubs, createHub, updateHub } from 'pages/api/api';
// ============================================================================
-// Design tokens — mirrors the DT block used across the console (see
-// CLAUDE.md §6). Brand red #C01227 is the canonical primary.
+// DT token block — CLAUDE.md §6, copied verbatim.
// ============================================================================
const DT = {
radiusPill: 999,
@@ -55,7 +42,6 @@ const DT = {
radiusInner: 12,
shadowSoft: '0 14px 40px rgba(15, 23, 42, 0.10)',
shadowMd: '0 8px 24px rgba(15, 23, 42, 0.08)',
- shadowPop: '0 18px 50px rgba(15, 23, 42, 0.18)',
textPrimary: '#0f172a',
textSecondary: '#64748b',
textMuted: '#94a3b8',
@@ -72,7 +58,18 @@ const edge = (c) => a(c, '55');
const BRAND = '#C01227';
-const CITIES = ['Coimbatore', 'Hyderabad', 'Bangalore', 'Chennai'];
+// Confirmed from the backend test: hubs carry `applocationid` (1–4), not a
+// literal city string. Everything else here (contact/address/pincode as
+// editable fields) is inferred from the create-body shape, not confirmed
+// present on the GET response — flagged in the summary, not guessed silently.
+const CITY_MAP = {
+ 1: { name: 'Coimbatore', code: 'CBE', color: '#f59e0b' },
+ 2: { name: 'Hyderabad', code: 'HYD', color: '#06b6d4' },
+ 3: { name: 'Bangalore', code: 'BLR', color: '#8b5cf6' },
+ 4: { name: 'Chennai', code: 'CHN', color: '#10b981' }
+};
+const CITY_NAME_TO_ID = Object.fromEntries(Object.entries(CITY_MAP).map(([id, c]) => [c.name, Number(id)]));
+
const HUB_TYPES = [
{ value: 'sorting_center', label: 'Sorting Center' },
{ value: 'spoke', label: 'Spoke' },
@@ -93,13 +90,14 @@ const AccentAvatar = ({ color, selected, size = 24, children }) => (
);
-const EMPTY_FORM = { hubname: '', hubtype: 'sorting_center', city: CITIES[0], capacity: '', contact: '', address: '', pincode: '' };
+const EMPTY_FORM = { hubname: '', hubtype: 'sorting_center', city: 'Coimbatore', capacity: '', contact: '', address: '', pincode: '' };
+
+const fetchAllMilers = () =>
+ axios.get(`${process.env.REACT_APP_URL}/admin/milers`).then((r) => r.data?.data || []);
const Hubs = () => {
- const theme = useTheme();
- const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const queryClient = useQueryClient();
- const [cityFilter, setCityFilter] = useState('All');
+ const [selectedCity, setSelectedCity] = useState('All');
const [dialogOpen, setDialogOpen] = useState(false);
const [editingHub, setEditingHub] = useState(null);
const [form, setForm] = useState(EMPTY_FORM);
@@ -110,14 +108,10 @@ const Hubs = () => {
queryFn: fetchHubs
});
- // Milers filtered per-hub for the expandable row. /admin/milers has no
- // server-side hub filter (same limitation noted in api.js for fetchAllRiders),
- // so this fetches the full list once and filters client-side.
- const { data: allMilersRes } = useQuery({
- queryKey: ['fetchAllRiders', 0, '', 0],
- queryFn: fetchAllRiders
+ const { data: milers = [] } = useQuery({
+ queryKey: ['fetchAllMilers'],
+ queryFn: fetchAllMilers
});
- const allMilers = allMilersRes?.details || [];
const createMutation = useMutation({
mutationFn: createHub,
@@ -142,21 +136,14 @@ const Hubs = () => {
onError: (err) => OpenToast(err.response?.data?.message || err.message, 'error', 3000)
});
- const filteredHubs = cityFilter === 'All' ? hubs : hubs.filter((h) => h.city === cityFilter);
+ const filteredHubs =
+ selectedCity === 'All' ? hubs : hubs.filter((h) => CITY_MAP[h.applocationid]?.name === selectedCity);
const kpis = [
- { key: 'total', label: 'Total Hubs', color: BRAND, icon: MdOutlineWarehouse, value: hubs.length },
- { key: 'active', label: 'Active', color: '#10b981', icon: MdOutlineCheckCircle, value: hubs.filter((h) => (h.status || 'active').toLowerCase() === 'active').length },
- {
- key: 'capacity',
- label: 'Capacity Used',
- color: '#f59e0b',
- icon: MdOutlineBarChart,
- value: hubs.length
- ? `${Math.round((hubs.reduce((s, h) => s + (h.usedcapacity || 0), 0) / (hubs.reduce((s, h) => s + (h.capacity || 0), 0) || 1)) * 100)}%`
- : '0%'
- },
- { key: 'cities', label: 'Cities', color: '#0ea5e9', icon: MdOutlineLocationCity, value: new Set(hubs.map((h) => h.city).filter(Boolean)).size }
+ { key: 'total', label: 'Total Hubs', color: BRAND, value: hubs.length },
+ { key: 'sorting', label: 'Sorting Centers', color: '#3b82f6', value: hubs.filter((h) => h.hubtype === 'sorting_center').length },
+ { key: 'spokes', label: 'Spokes', color: '#14b8a6', value: hubs.filter((h) => h.hubtype === 'spoke').length },
+ { key: 'cities', label: 'Cities Covered', color: '#10b981', value: new Set(hubs.map((h) => h.applocationid).filter(Boolean)).size }
];
const openCreate = () => {
@@ -169,8 +156,8 @@ const Hubs = () => {
setForm({
hubname: hub.hubname || '',
hubtype: hub.hubtype || 'sorting_center',
- city: hub.city || CITIES[0],
- capacity: hub.capacity || '',
+ city: CITY_MAP[hub.applocationid]?.name || 'Coimbatore',
+ capacity: hub.capacity ?? '',
contact: hub.contact || '',
address: hub.address || '',
pincode: hub.pincode || ''
@@ -183,10 +170,19 @@ const Hubs = () => {
OpenToast('Enter a hub name', 'warning', 2000);
return;
}
+ const body = {
+ hubname: form.hubname,
+ hubtype: form.hubtype,
+ applocationid: CITY_NAME_TO_ID[form.city],
+ capacity: form.capacity === '' ? undefined : Number(form.capacity),
+ contact: form.contact,
+ address: form.address,
+ pincode: form.pincode
+ };
if (editingHub) {
- updateMutation.mutate({ id: editingHub.hubid, body: form });
+ updateMutation.mutate({ id: editingHub.hubid, body });
} else {
- createMutation.mutate(form);
+ createMutation.mutate(body);
}
};
@@ -198,7 +194,7 @@ const Hubs = () => {
sx={{
p: { xs: 2, md: 3 },
borderRadius: `${DT.radiusCard}px`,
- background: `linear-gradient(135deg, ${tint(BRAND)} 0%, ${tint('#D35968')} 100%)`,
+ background: 'linear-gradient(135deg, #C012270A 0%, #D359680A 100%)',
border: '1px solid',
borderColor: DT.borderSubtle,
mb: { xs: 1.5, md: 2 }
@@ -207,30 +203,30 @@ const Hubs = () => {
-
+
Hubs
- Live · {cityFilter}
+ Live · {selectedCity}
- {['All', ...CITIES].map((c) => (
+ {['All', ...Object.values(CITY_MAP).map((c) => c.name)].map((c) => (
setCityFilter(c)}
+ onClick={() => setSelectedCity(c)}
sx={{
fontWeight: 700,
- bgcolor: cityFilter === c ? BRAND : '#fff',
- color: cityFilter === c ? '#fff' : DT.textSecondary,
- border: `1px solid ${cityFilter === c ? BRAND : DT.borderSubtle}`,
- '&:hover': { bgcolor: cityFilter === c ? BRAND : DT.surfaceAlt }
+ bgcolor: selectedCity === c ? BRAND : '#fff',
+ color: selectedCity === c ? '#fff' : DT.textSecondary,
+ border: `1px solid ${selectedCity === c ? BRAND : DT.borderSubtle}`,
+ '&:hover': { bgcolor: selectedCity === c ? BRAND : DT.surfaceAlt }
}}
/>
))}
@@ -240,14 +236,29 @@ const Hubs = () => {
{/* ============================================= || KPI Cards || ============================================= */}
- {kpis.map((item) => {
- const Icon = item.icon;
- return (
-
- } color={item.color} loading={hubsLoading} />
-
- );
- })}
+ {kpis.map((item) => (
+
+
+
+ {item.label}
+
+
+ {hubsLoading ? '—' : item.value}
+
+
+
+ ))}
{/* ============================================= || New Hub || ============================================= */}
@@ -264,7 +275,7 @@ const Hubs = () => {
{/* ============================================= || Table || ============================================= */}
-
+
{
City
Type
Capacity
+ Milers
Status
- Milers Assigned
Actions
@@ -309,48 +320,35 @@ const Hubs = () => {
)}
{filteredHubs.map((hub) => {
- const hubMilers = allMilers.filter((m) => m.hubid === hub.hubid);
+ const hubMilers = milers.filter((m) => m.hubid === hub.hubid);
const expanded = expandedHubId === hub.hubid;
+ const city = CITY_MAP[hub.applocationid];
+ const isActive = (hub.status || 'active').toLowerCase() === 'active';
return (
-
+
-
-
-
-
-
- {hub.hubname || '—'}
-
-
-
-
-
- {hub.city || '—'}
+
+ {hub.hubname || '—'}
- t.value === hub.hubtype)?.label || hub.hubtype || '—'}
- sx={{ bgcolor: tint('#0ea5e9'), color: '#0ea5e9', border: `1px solid ${edge('#0ea5e9')}`, fontWeight: 700 }}
- />
+ {city ? (
+
+ ) : (
+
+ —
+
+ )}
+
+
+
+ {(hub.hubtype || '—').replace(/_/g, ' ')}
+
{hub.capacity ?? '—'}
-
-
-
{
/>
+
+
+
@@ -391,17 +401,55 @@ const Hubs = () => {
No milers assigned to this hub.
) : (
-
- {hubMilers.map((m) => (
- }
- label={`${m.displayname || `Miler #${m.userid}`} · ${m.availabilitystatus || '—'}`}
- sx={{ bgcolor: '#fff', border: `1px solid ${DT.borderSubtle}`, fontWeight: 600 }}
- />
- ))}
-
+
+
+
+
+ Miler
+ Phone
+ Status
+ Rating
+
+
+
+ {hubMilers.map((m) => {
+ const availColor = m.availabilitystatus === 'Available' ? '#10b981' : m.availabilitystatus === 'On_Break' ? '#f59e0b' : '#94a3b8';
+ return (
+
+
+
+
+
+
+
+ {m.displayname || `Miler #${m.userid}`}
+
+
+
+
+
+ {m.phone || '—'}
+
+
+
+
+
+
+
+
+ {m.rating ?? '—'}
+
+
+
+ );
+ })}
+
+
+
)}
@@ -416,9 +464,9 @@ const Hubs = () => {
{/* ============================================= || Create / Edit dialog || ============================================= */}
-