updates on the setting page
This commit is contained in:
@@ -51,6 +51,7 @@ const RidersSummary = lazy(() => import('@/pages/doormile/reports/RidersSummary'
|
||||
const Profitability = lazy(() => import('@/pages/doormile/reports/Profitability'));
|
||||
|
||||
const ViewProfile = lazy(() => import('@/pages/doormile/ViewProfile'));
|
||||
const Settings = lazy(() => import('@/pages/doormile/settings/Settings'));
|
||||
|
||||
const PageFallback = () => (
|
||||
<div className="flex min-h-[60vh] items-center justify-center">
|
||||
@@ -148,6 +149,7 @@ const AuthenticatedApp = () => {
|
||||
<Route path="reports/profitability" element={<Profitability />} />
|
||||
|
||||
<Route path="profile" element={<ViewProfile />} />
|
||||
<Route path="settings" element={<Settings />} />
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Link, Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
Activity, Bell, Bike, Car, ChevronDown, Coins, FileSpreadsheet,
|
||||
FileText, IndianRupee, ListTodo, LogOut, Menu, Search, Settings, Shield,
|
||||
FileText, ListTodo, LogOut, Menu, Search, Settings, Shield,
|
||||
ShieldAlert, User, UserCheck, Users, Warehouse,
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
@@ -79,27 +79,6 @@ const NAV_GROUPS = [
|
||||
},
|
||||
];
|
||||
|
||||
/* Configuration destinations, kept off the operations bar.
|
||||
*
|
||||
* Declared here rather than written inline in the account dropdown because
|
||||
* two surfaces need them: that dropdown on desktop, and the mobile sheet.
|
||||
* When they lived only in the dropdown's JSX, moving them off the bar quietly
|
||||
* removed them from the phone entirely — the sheet builds its list from NAV
|
||||
* plus NAV_GROUPS, and these were in neither. Both pages stayed routed and
|
||||
* reachable by typed URL, which is the kind of gap nobody reports and nobody
|
||||
* finds.
|
||||
*
|
||||
* (This replaces an ALL_DESTINATIONS constant that flattened NAV and
|
||||
* NAV_GROUPS. Its comment claimed it was "what the mobile sheet renders"; the
|
||||
* sheet had moved to a role-filtered local of the same name in lower case, so
|
||||
* the constant had no readers at all and would have reintroduced the same
|
||||
* omission if anyone had trusted it.)
|
||||
*/
|
||||
const SETTINGS_NAV = [
|
||||
{ label: 'Pricing Matrix', path: '/doormile/pricing', icon: IndianRupee },
|
||||
{ label: 'Customers Directory', path: '/doormile/customers', icon: Users },
|
||||
];
|
||||
|
||||
function GlobalSearch() {
|
||||
const navigate = useNavigate();
|
||||
const [query, setQuery] = useState('');
|
||||
@@ -492,18 +471,15 @@ export default function AdminLayout() {
|
||||
<Shield className="mr-2.5 h-4 w-4 text-slate-500" /> Change password
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator className="my-1" />
|
||||
<div className="px-2.5 py-1 text-[10px] font-bold uppercase tracking-wider text-slate-400 flex items-center gap-1.5">
|
||||
<Settings className="w-3 h-3 text-slate-400" /> Settings
|
||||
</div>
|
||||
{SETTINGS_NAV.map((item) => (
|
||||
<DropdownMenuItem
|
||||
key={item.path}
|
||||
onClick={() => navigate(item.path)}
|
||||
className="cursor-pointer rounded-xl px-2.5 py-2 font-medium"
|
||||
>
|
||||
<item.icon className="mr-2.5 h-4 w-4 text-slate-500" /> {item.label}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuItem
|
||||
onClick={() => navigate('/doormile/settings')}
|
||||
className={cn(
|
||||
'cursor-pointer rounded-xl px-2.5 py-2 font-medium',
|
||||
location.pathname.startsWith('/doormile/settings') && 'bg-brand-tint text-brand font-semibold'
|
||||
)}
|
||||
>
|
||||
<Settings className="mr-2.5 h-4 w-4 text-slate-500" /> Settings
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator className="my-1" />
|
||||
<DropdownMenuItem
|
||||
onClick={() => logout()}
|
||||
@@ -552,22 +528,16 @@ export default function AdminLayout() {
|
||||
Without this block they existed on the bar's dropdown only, so
|
||||
a phone could not reach Pricing or Customers at all. */}
|
||||
<div className="my-2 h-px bg-border" />
|
||||
<div className="px-3 pb-1 text-[10px] font-bold uppercase tracking-wider text-ink-3">
|
||||
<Link
|
||||
to="/doormile/settings"
|
||||
onClick={() => setMenuOpen(false)}
|
||||
className={cn(
|
||||
'rounded-lg px-3 py-2.5 text-body-sm font-medium transition-colors',
|
||||
isActive('/doormile/settings') ? 'bg-brand-tint text-brand font-semibold' : 'text-ink-2 hover:bg-surface-sunken'
|
||||
)}
|
||||
>
|
||||
Settings
|
||||
</div>
|
||||
{SETTINGS_NAV.map((item) => (
|
||||
<Link
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
onClick={() => setMenuOpen(false)}
|
||||
className={cn(
|
||||
'rounded-lg px-3 py-2.5 text-body-sm font-medium transition-colors',
|
||||
isActive(item.path) ? 'bg-brand-tint text-brand' : 'text-ink-2 hover:bg-surface-sunken'
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
</Link>
|
||||
<div className="my-2 h-px bg-border" />
|
||||
<Link
|
||||
to="/doormile/profile"
|
||||
|
||||
@@ -324,6 +324,8 @@ export const useDeleteVehicle = () =>
|
||||
export const useMilers = (options) =>
|
||||
useQuery({ queryKey: KEYS.milers, queryFn: api.getallriders, staleTime: 30_000, ...options });
|
||||
|
||||
export const useRiders = useMilers;
|
||||
|
||||
export const useMiler = (id, options) =>
|
||||
useQuery({ queryKey: KEYS.miler(id), queryFn: () => api.getMiler(id), enabled: !!id, ...options });
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ const custEmail = (row) => row.email || '';
|
||||
/** Tenant customers carry no booking count; 0 is honest rather than blank. */
|
||||
const custBookings = (row) => row.totalbookings ?? 0;
|
||||
|
||||
export default function Customers() {
|
||||
export default function Customers({ embedded = false }) {
|
||||
const navigate = useNavigate();
|
||||
const { data: customers = [], isLoading, isFetching } = useTenantCustomers();
|
||||
const updateCustomer = useUpdateTenantCustomer();
|
||||
@@ -141,13 +141,15 @@ export default function Customers() {
|
||||
|
||||
return (
|
||||
<Stack space="lg">
|
||||
<PageHeader
|
||||
actions={
|
||||
<Button onClick={() => navigate('/doormile/customer/create')}>
|
||||
<Plus className="mr-1.5 h-4 w-4" /> New customer
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
{!embedded && (
|
||||
<PageHeader
|
||||
actions={
|
||||
<Button onClick={() => navigate('/doormile/customer/create')}>
|
||||
<Plus className="mr-1.5 h-4 w-4" /> New customer
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Grid cols={4}>
|
||||
<KpiCard
|
||||
|
||||
@@ -25,8 +25,10 @@ import { currency, km as formatKm, number } from '@/lib/doormileFormat';
|
||||
* what is shown here, which makes these margins the conservative read.
|
||||
*/
|
||||
|
||||
export default function Profitability() {
|
||||
const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
|
||||
export default function Profitability({ embedded = false, date: controlledDate, onDateChange }) {
|
||||
const [internalDate, setInternalDate] = useState(dayjs().format('YYYY-MM-DD'));
|
||||
const date = controlledDate ?? internalDate;
|
||||
const setDate = onDateChange ?? setInternalDate;
|
||||
const [page, setPage] = useState(1);
|
||||
const [collected, setCollected] = useState([]);
|
||||
|
||||
@@ -158,17 +160,19 @@ export default function Profitability() {
|
||||
|
||||
return (
|
||||
<Stack space="lg">
|
||||
<PageHeader
|
||||
actions={
|
||||
<Input
|
||||
type="date"
|
||||
value={date}
|
||||
onChange={(e) => e.target.value && setDate(e.target.value)}
|
||||
aria-label="Report date"
|
||||
className="w-44"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{!embedded && (
|
||||
<PageHeader
|
||||
actions={
|
||||
<Input
|
||||
type="date"
|
||||
value={date}
|
||||
onChange={(e) => e.target.value && setDate(e.target.value)}
|
||||
aria-label="Report date"
|
||||
className="w-44"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Grid cols={4}>
|
||||
<KpiCard icon={IndianRupee} value={currency(totals.revenue, '₹0')} label="Revenue" tone="brand" index={0} loading={isLoading} />
|
||||
|
||||
129
src/pages/doormile/settings/Settings.jsx
Normal file
129
src/pages/doormile/settings/Settings.jsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useSearchParams, useNavigate } from 'react-router-dom';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import dayjs from 'dayjs';
|
||||
import { TrendingUp, Users, Plus } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Button, Input } from '@/components/ds';
|
||||
import Customers from '@/pages/doormile/customers/Customers';
|
||||
import Profitability from '@/pages/doormile/reports/Profitability';
|
||||
|
||||
const TABS = [
|
||||
{ id: 'profitability', label: 'Profitability', icon: TrendingUp },
|
||||
{ id: 'customers', label: 'Customers', icon: Users },
|
||||
];
|
||||
|
||||
export default function Settings() {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const tabParam = searchParams.get('tab');
|
||||
|
||||
// Default to 'profitability' or respect ?tab=
|
||||
const [activeTab, setActiveTab] = useState(
|
||||
tabParam && TABS.some((t) => t.id === tabParam) ? tabParam : 'profitability'
|
||||
);
|
||||
|
||||
// Maintain shared date for Profitability
|
||||
const [reportDate, setReportDate] = useState(dayjs().format('YYYY-MM-DD'));
|
||||
|
||||
// Sync state if URL query param changes
|
||||
useEffect(() => {
|
||||
if (tabParam && TABS.some((t) => t.id === tabParam)) {
|
||||
setActiveTab(tabParam);
|
||||
}
|
||||
}, [tabParam]);
|
||||
|
||||
const handleTabChange = (id) => {
|
||||
setActiveTab(id);
|
||||
setSearchParams({ tab: id });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4 max-w-7xl mx-auto w-full pb-16">
|
||||
{/* Header — Title and Subtitle */}
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-tight text-slate-900 font-heading">
|
||||
Settings
|
||||
</h1>
|
||||
<p className="text-sm text-slate-500 mt-0.5">
|
||||
Workspace configuration and management for Doormile Express.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Unified Toolbar: Segmented Tabs on Left, Tab-Specific Action on Right */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 pt-1">
|
||||
{/* Pill Segmented Tabs */}
|
||||
<div className="inline-flex items-center gap-1 p-1 bg-slate-100/90 rounded-2xl border border-slate-200/80 shadow-2xs">
|
||||
{TABS.map((tab) => {
|
||||
const Icon = tab.icon;
|
||||
const isActive = activeTab === tab.id;
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
type="button"
|
||||
onClick={() => handleTabChange(tab.id)}
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-4 py-2 rounded-xl text-xs sm:text-sm font-medium transition-all whitespace-nowrap cursor-pointer select-none',
|
||||
isActive
|
||||
? 'bg-white text-slate-900 font-semibold shadow-xs border border-slate-200/80'
|
||||
: 'text-slate-600 hover:text-slate-900 hover:bg-slate-200/50'
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
className={cn(
|
||||
'w-4 h-4 transition-colors',
|
||||
isActive ? 'text-brand' : 'text-slate-500'
|
||||
)}
|
||||
/>
|
||||
<span>{tab.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Tab Context Action aligned directly on the right */}
|
||||
<div className="flex items-center gap-2">
|
||||
{activeTab === 'profitability' && (
|
||||
<Input
|
||||
type="date"
|
||||
value={reportDate}
|
||||
onChange={(e) => e.target.value && setReportDate(e.target.value)}
|
||||
aria-label="Report date"
|
||||
className="w-44 bg-white shadow-xs"
|
||||
/>
|
||||
)}
|
||||
{activeTab === 'customers' && (
|
||||
<Button
|
||||
onClick={() => navigate('/doormile/customer/create')}
|
||||
className="inline-flex items-center gap-1.5"
|
||||
>
|
||||
<Plus className="h-4 w-4" /> New customer
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab Panels with seamless integration */}
|
||||
<div className="pt-1">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={activeTab}
|
||||
initial={{ opacity: 0, y: 4 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -4 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
>
|
||||
{activeTab === 'profitability' && (
|
||||
<Profitability
|
||||
embedded
|
||||
date={reportDate}
|
||||
onDateChange={setReportDate}
|
||||
/>
|
||||
)}
|
||||
{activeTab === 'customers' && <Customers embedded />}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user