updates on auth for the login creds and data linkage and updates on the deliveries page and queries section and updates on the test files

This commit is contained in:
2026-08-28 11:38:53 +05:30
parent f48e061138
commit 5d5a748352
5 changed files with 93 additions and 7 deletions

View File

@@ -142,6 +142,13 @@ export const deriveDeliveryStatus = (bookingOrStatus, consignmentMapOrReachedAt)
// - 'cancelled' -> 'cancelled' (Cancelled tab)
if (cStatus) {
const rawC = String(cStatus).trim().toLowerCase();
// A booking sitting at converted_to_consignment / collected_by_miler is in the 'picked'
// state (parcel in rider's hand after pickup). Generic DB record status 'active' on the
// consignment must not preemptively bump it to 'active' (Out for Delivery) until the
// consignment is explicitly marked 'out_for_delivery' / 'in_transit' / etc.
if (rawC === 'active' && (rawBookingStatus === 'converted_to_consignment' || rawBookingStatus === 'collected_by_miler' || rawBookingStatus === 'picked')) {
return 'picked';
}
const mappedC = BOOKING_STATUS_TO_DELIVERY_STATUS[rawC];
if (mappedC) return mappedC;
}
@@ -179,7 +186,9 @@ const consignmentStatusFor = (booking, consignmentMap) => {
if (!consignmentId || !consignmentMap?.size) return undefined;
const record = consignmentMap.get(String(consignmentId));
if (!record) return undefined;
const raw = record.status ?? record.consignmentstatus ?? record.currentstatus ?? record.consignment_status;
// Specific consignment lifecycle fields (consignmentstatus / consignment_status / currentstatus)
// MUST take precedence over the generic DB entity status column (record.status).
const raw = record.consignmentstatus ?? record.consignment_status ?? record.currentstatus ?? record.status;
return typeof raw === 'string' && raw.trim() ? raw.trim() : undefined;
};

View File

@@ -222,10 +222,28 @@ function NotificationsMenu() {
export default function AdminLayout() {
const location = useLocation();
const navigate = useNavigate();
const { user, logout } = useAuth();
const { user, isClient, logout } = useAuth();
const [menuOpen, setMenuOpen] = useState(false);
const [assistantOpen, setAssistantOpen] = useState(false);
const navItems = useMemo(() => {
if (isClient) {
return NAV.filter((item) => item.path !== '/doormile/tenants');
}
return NAV;
}, [isClient]);
const navGroups = useMemo(() => {
if (isClient) {
return NAV_GROUPS.filter((group) => group.label !== 'Fleet Ops');
}
return NAV_GROUPS;
}, [isClient]);
const allDestinations = useMemo(() => {
return [...navItems, ...navGroups.flatMap((group) => group.items)];
}, [navItems, navGroups]);
const isActive = (path) => {
if (path === ROOT) return location.pathname === ROOT;
return location.pathname === path || location.pathname.startsWith(`${path}/`);
@@ -269,7 +287,7 @@ export default function AdminLayout() {
menu is styled as one more tab, so a destination reached through a
menu still reads as being in the same row as the rest. */}
<nav aria-label="Console" className="hidden min-w-0 flex-1 items-center gap-0.5 lg:flex">
{NAV.map((item) => (
{navItems.map((item) => (
<Link
key={item.path}
to={item.path}
@@ -291,7 +309,7 @@ export default function AdminLayout() {
</Link>
))}
{NAV_GROUPS.map((group) => (
{navGroups.map((group) => (
<DropdownMenu key={group.label}>
<DropdownMenuTrigger asChild>
<button
@@ -348,7 +366,7 @@ export default function AdminLayout() {
<img className="h-9 w-9 max-w-none object-contain" src={doormileMark} alt="" aria-hidden="true" />
</button>
<NotificationsMenu />
{!isClient && <NotificationsMenu />}
<DropdownMenu>
<DropdownMenuTrigger asChild>
@@ -409,7 +427,7 @@ export default function AdminLayout() {
<SheetTitle className="text-left font-heading text-title">Doormile Express</SheetTitle>
</SheetHeader>
<nav className="flex max-h-[calc(100dvh-8rem)] flex-col overflow-y-auto p-2">
{ALL_DESTINATIONS.map((item) => (
{allDestinations.map((item) => (
<Link
key={item.path}
to={item.path}

View File

@@ -23,6 +23,26 @@ import {
* shell that renders but 401s on every fetch is worse than being sent back.
*/
/**
* Determines whether the signed-in user is a client/merchant account
* rather than Doormile internal staff.
*/
export const isClientUser = (user) => {
if (!user) {
const storedTenant = localStorage.getItem('tenantid');
const storedAuth = (localStorage.getItem('authname') || '').toLowerCase();
if (storedTenant && storedTenant !== '0' && storedTenant !== 'null' && storedTenant !== '') return true;
if (storedAuth.includes('dailygrubs.com')) return true;
return false;
}
const email = (user.email || user.primaryemail || user.authname || '').toLowerCase();
if (email.includes('dailygrubs.com') || email === 'info@dailygrubs.com') return true;
if (user.tenantid != null && user.tenantid !== '' && user.tenantid !== 0 && user.tenantid !== '0') return true;
const role = String(user.role || user.roleid || '').toLowerCase();
if (role === 'client' || role === 'tenant' || role === 'merchant') return true;
return false;
};
const AuthContext = createContext(/** @type {any} */ (null));
/** @param {any} props */
@@ -90,9 +110,12 @@ export const AuthProvider = ({ children }) => {
const navigateToLogin = useCallback(() => window.location.replace('/login'), []);
const isClient = useMemo(() => isClientUser(user), [user]);
const value = useMemo(
() => ({
user,
isClient,
isAuthenticated,
isLoadingAuth,
/* No remote app settings on this backend — kept so consumers that gate on
@@ -107,7 +130,7 @@ export const AuthProvider = ({ children }) => {
checkUserAuth,
checkAppState: checkUserAuth,
}),
[user, isAuthenticated, isLoadingAuth, authError, authChecked, login, logout, navigateToLogin, checkUserAuth]
[user, isClient, isAuthenticated, isLoadingAuth, authError, authChecked, login, logout, navigateToLogin, checkUserAuth]
);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;

View File

@@ -193,6 +193,16 @@ describe('deriveDeliveryStatus', () => {
it('should still return picked for a Collected_By_Miler booking with no consignment', () => {
expect(deriveDeliveryStatus({ status: 'Collected_By_Miler' })).toBe('picked');
});
it('should keep Converted_To_Consignment as picked when joined consignment only has generic status: Active', () => {
const map = new Map([['40', { status: 'Active' }]]);
expect(deriveDeliveryStatus({ status: 'Converted_To_Consignment', consignmentid: 40 }, map)).toBe('picked');
});
it('should bump Converted_To_Consignment to active when joined consignment has consignmentstatus: Out_for_Delivery', () => {
const map = new Map([['40', { status: 'Active', consignmentstatus: 'Out_for_Delivery' }]]);
expect(deriveDeliveryStatus({ status: 'Converted_To_Consignment', consignmentid: 40 }, map)).toBe('active');
});
});
describe('mapBookingStatusToDeliveryStatus', () => {

View File

@@ -270,6 +270,32 @@ describe('AuthContext', () => {
});
});
describe('isClient role determination', () => {
it('should detect client for info@dailygrubs.com', async () => {
readStoredToken.mockReturnValue('jwt-abc');
readStoredUser.mockReturnValue({ email: 'info@dailygrubs.com', tenantid: 12 });
const { result } = renderAuth();
await waitFor(() => expect(result.current.authChecked).toBe(true));
expect(result.current.isClient).toBe(true);
});
it('should detect client for user with tenantid', async () => {
readStoredToken.mockReturnValue('jwt-abc');
readStoredUser.mockReturnValue({ email: 'client@partner.com', tenantid: 5 });
const { result } = renderAuth();
await waitFor(() => expect(result.current.authChecked).toBe(true));
expect(result.current.isClient).toBe(true);
});
it('should not mark super admin / staff with tenantid 0 as client', async () => {
readStoredToken.mockReturnValue('jwt-abc');
readStoredUser.mockReturnValue({ email: 'admin@doormile.com', tenantid: '0' });
const { result } = renderAuth();
await waitFor(() => expect(result.current.authChecked).toBe(true));
expect(result.current.isClient).toBe(false);
});
});
describe('useAuth outside a provider', () => {
it('should throw a named error rather than returning undefined', () => {
// Silent undefined here would render an unauthenticated shell as if it