updates on the setting page

This commit is contained in:
2026-09-19 13:00:27 +05:30
parent a044afc614
commit 3e2e9cad3a
6 changed files with 179 additions and 70 deletions

View File

@@ -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>

View File

@@ -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"

View File

@@ -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 });

View File

@@ -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

View File

@@ -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} />

View 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>
);
}