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:
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user