updateson the create ordera nd testfile as done
This commit is contained in:
@@ -192,18 +192,47 @@ export default function CreateOrder() {
|
|||||||
const [searchCustQuery, setSearchCustQuery] = useState('');
|
const [searchCustQuery, setSearchCustQuery] = useState('');
|
||||||
|
|
||||||
// Local persistent saved contacts for new/recent customers
|
// Local persistent saved contacts for new/recent customers
|
||||||
const [savedContacts, setSavedContacts] = useState(() => {
|
const readSavedContacts = () => {
|
||||||
try {
|
try {
|
||||||
const data = localStorage.getItem('doormile_saved_contacts');
|
const data = localStorage.getItem('doormile_saved_contacts');
|
||||||
return data ? JSON.parse(data) : [];
|
return data ? JSON.parse(data) : [];
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
const [savedContacts, setSavedContacts] = useState(readSavedContacts);
|
||||||
|
|
||||||
|
// Re-read the browser copy when it changes elsewhere. It used to be read
|
||||||
|
// once, on mount: a contact saved in another tab never appeared in a Create
|
||||||
|
// Order tab that was already open (clicking "Create order" again doesn't
|
||||||
|
// remount it). `storage` fires for other tabs, the custom event for this
|
||||||
|
// one, and focus covers anything missed while the tab was in the background.
|
||||||
|
useEffect(() => {
|
||||||
|
const refresh = () => setSavedContacts(readSavedContacts());
|
||||||
|
const onStorage = (e) => {
|
||||||
|
if (!e.key || e.key === 'doormile_saved_contacts') refresh();
|
||||||
|
};
|
||||||
|
window.addEventListener('storage', onStorage);
|
||||||
|
window.addEventListener('doormile_saved_contacts_updated', refresh);
|
||||||
|
window.addEventListener('focus', refresh);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('storage', onStorage);
|
||||||
|
window.removeEventListener('doormile_saved_contacts_updated', refresh);
|
||||||
|
window.removeEventListener('focus', refresh);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const [saveForLater, setSaveForLater] = useState(true);
|
const [saveForLater, setSaveForLater] = useState(true);
|
||||||
const [isSavedNow, setIsSavedNow] = useState(false);
|
const [isSavedNow, setIsSavedNow] = useState(false);
|
||||||
|
|
||||||
|
// "Saved" describes the contact currently in the form. Editing the name,
|
||||||
|
// phone or address makes it a different contact, so the button goes back to
|
||||||
|
// "Save Contact" — it used to stay "Saved" and suggest the new details were
|
||||||
|
// already stored when they weren't.
|
||||||
|
useEffect(() => {
|
||||||
|
setIsSavedNow(false);
|
||||||
|
}, [dropCust.firstname, dropCust.contactno, dropCust.address]);
|
||||||
|
|
||||||
// Map pin picker modal target ('pickup' | 'drop' | null)
|
// Map pin picker modal target ('pickup' | 'drop' | null)
|
||||||
const [mapPickerTarget, setMapPickerTarget] = useState(null);
|
const [mapPickerTarget, setMapPickerTarget] = useState(null);
|
||||||
|
|
||||||
@@ -246,14 +275,34 @@ export default function CreateOrder() {
|
|||||||
[hubList, tenantLocations]
|
[hubList, tenantLocations]
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: customerList = [], isLoading: isCustomersLoading } = useQuery({
|
const { data: customerList = [], isLoading: isAdminCustomersLoading } = useQuery({
|
||||||
queryKey: ['adminCustomers'],
|
queryKey: ['adminCustomers'],
|
||||||
queryFn: getAdminCustomers
|
queryFn: getAdminCustomers
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The store "save for later" writes to (POST /admin/tenantcustomers) and the
|
||||||
|
// Customers page reads. The picker used to read only /admin/customers — the
|
||||||
|
// customer-app accounts — so a contact saved on the server never showed up
|
||||||
|
// here; only this browser's local copy did, which another browser, profile
|
||||||
|
// or incognito window can't see. Same query key as the Customers page, so
|
||||||
|
// the two share one cached list.
|
||||||
|
const { data: tenantCustomerList = [], isLoading: isTenantCustomersLoading } = useQuery({
|
||||||
|
queryKey: ['doormile', 'tenant-customers'],
|
||||||
|
queryFn: getTenantCustomers,
|
||||||
|
staleTime: 60_000
|
||||||
|
});
|
||||||
|
const isCustomersLoading = isAdminCustomersLoading || isTenantCustomersLoading;
|
||||||
|
|
||||||
// Combined customers: Merge server customers with locally saved/recent customer contacts
|
// Combined customers: Merge server customers with locally saved/recent customer contacts
|
||||||
const allCustomers = useMemo(() => {
|
const allCustomers = useMemo(() => {
|
||||||
const list = [...(customerList || [])];
|
// Tenant customers carry firstname/lastname; the picker renders `name`.
|
||||||
|
const savedOnServer = (tenantCustomerList || []).map((c) => ({
|
||||||
|
...c,
|
||||||
|
name: c.name || [c.firstname, c.lastname].filter(Boolean).join(' ') || undefined,
|
||||||
|
phone: c.phone || c.contactno,
|
||||||
|
isSavedForLater: true
|
||||||
|
}));
|
||||||
|
const list = [...savedOnServer, ...(customerList || [])];
|
||||||
const seenPhones = new Set();
|
const seenPhones = new Set();
|
||||||
const result = [];
|
const result = [];
|
||||||
|
|
||||||
@@ -276,7 +325,7 @@ export default function CreateOrder() {
|
|||||||
result.push(c);
|
result.push(c);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}, [customerList, savedContacts]);
|
}, [customerList, tenantCustomerList, savedContacts]);
|
||||||
|
|
||||||
const matchedCustomer = useMemo(() => {
|
const matchedCustomer = useMemo(() => {
|
||||||
const cleanPhone = String(dropCust.contactno || '').replace(/\D/g, '');
|
const cleanPhone = String(dropCust.contactno || '').replace(/\D/g, '');
|
||||||
@@ -1818,7 +1867,7 @@ export default function CreateOrder() {
|
|||||||
const hasPin = Boolean(cust.latitude && cust.longitude);
|
const hasPin = Boolean(cust.latitude && cust.longitude);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={cust.appcustomerid || cust.id || cust.phone || Math.random()}
|
key={cust.appcustomerid || cust.tenantcustomerid || cust.id || cust.phone || Math.random()}
|
||||||
className="p-3 hover:bg-slate-50 rounded-lg flex items-center justify-between gap-3 transition-colors"
|
className="p-3 hover:bg-slate-50 rounded-lg flex items-center justify-between gap-3 transition-colors"
|
||||||
>
|
>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
|
|||||||
125
tests/integration/createOrderSavedContacts.test.jsx
Normal file
125
tests/integration/createOrderSavedContacts.test.jsx
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "Save for later" on Create Order, then Saved Contacts in another tab.
|
||||||
|
*
|
||||||
|
* The contact didn't show up, for two reasons this pins:
|
||||||
|
* 1. The picker read only /admin/customers (customer-app accounts), while
|
||||||
|
* "save for later" writes to /admin/tenantcustomers — so a contact saved
|
||||||
|
* on the server never appeared; only the saving browser's local copy did.
|
||||||
|
* 2. The local copy was read once, on mount, so a Create Order tab that was
|
||||||
|
* already open never saw a contact saved in another tab.
|
||||||
|
*/
|
||||||
|
|
||||||
|
jest.mock('lucide-react', () =>
|
||||||
|
new Proxy({}, {
|
||||||
|
get: (_t, prop) => (prop === '__esModule' ? true : (props) => <span data-testid={`icon-${String(prop)}`} {...props} />),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
jest.mock('leaflet', () => ({ __esModule: true, default: { DivIcon: function DivIcon() {} } }));
|
||||||
|
jest.mock('react-leaflet', () => ({
|
||||||
|
MapContainer: () => null, Marker: () => null, Polyline: () => null, TileLayer: () => null, useMap: () => ({}),
|
||||||
|
}));
|
||||||
|
jest.mock('@/components/doormile/AddressAutocomplete', () => ({ __esModule: true, default: () => null }));
|
||||||
|
jest.mock('@/components/doormile/MapPinPickerModal', () => ({ __esModule: true, default: () => null }));
|
||||||
|
jest.mock('@/components/doormile/HubLocationSelect', () => ({ HubLocationSelect: () => null }));
|
||||||
|
jest.mock('@/lib/doormileHooks', () => ({ useHubs: () => ({ data: [] }) }));
|
||||||
|
jest.mock('@/lib/geocodingService', () => ({ geocodeAddress: jest.fn(), saveRecentAddress: jest.fn() }));
|
||||||
|
jest.mock('@/lib/distance', () => ({
|
||||||
|
calculateDrivingDistance: jest.fn(() => Promise.resolve(null)),
|
||||||
|
calculateDrivingRoute: jest.fn(() => Promise.resolve(null)),
|
||||||
|
calculateTotalCharge: jest.fn(() => 0),
|
||||||
|
}));
|
||||||
|
jest.mock('@/components/third-party/OpenToast', () => ({ OpenToast: jest.fn() }));
|
||||||
|
jest.mock('@/api/doormile/endpoints', () => ({
|
||||||
|
createExpressBooking: jest.fn(),
|
||||||
|
createTenantCustomer: jest.fn(),
|
||||||
|
getAdminCustomers: jest.fn(),
|
||||||
|
getAdminPricing: jest.fn(() => Promise.resolve([])),
|
||||||
|
getAdminTenants: jest.fn(() => Promise.resolve([])),
|
||||||
|
getTenantCustomers: jest.fn(),
|
||||||
|
getTenantLocations: jest.fn(() => Promise.resolve([])),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import CreateOrder from '@/pages/doormile/orders/CreateOrder';
|
||||||
|
import { getAdminCustomers, getTenantCustomers } from '@/api/doormile/endpoints';
|
||||||
|
|
||||||
|
const renderPage = () => {
|
||||||
|
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||||
|
return render(
|
||||||
|
<QueryClientProvider client={qc}>
|
||||||
|
<MemoryRouter>
|
||||||
|
<CreateOrder />
|
||||||
|
</MemoryRouter>
|
||||||
|
</QueryClientProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openSavedContacts = async () => {
|
||||||
|
fireEvent.click(screen.getAllByText('Saved Contacts')[0].closest('button'));
|
||||||
|
const heading = await screen.findByRole('heading', { name: 'Saved Contacts' });
|
||||||
|
return heading.closest('div.bg-white');
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
localStorage.clear();
|
||||||
|
getAdminCustomers.mockResolvedValue([
|
||||||
|
{ appcustomerid: 7, name: 'App Customer', phone: '9111111111' },
|
||||||
|
]);
|
||||||
|
getTenantCustomers.mockResolvedValue([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Create Order — Saved Contacts', () => {
|
||||||
|
it('shows a contact saved on the server, which this browser has never seen', async () => {
|
||||||
|
// Nothing in localStorage: this is the "other browser / incognito" case.
|
||||||
|
getTenantCustomers.mockResolvedValue([
|
||||||
|
{ tenantcustomerid: 42, firstname: 'Test', lastname: 'Save Contact', phone: '9000000001', address: '12 MG Road' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
renderPage();
|
||||||
|
const modal = await openSavedContacts();
|
||||||
|
|
||||||
|
await waitFor(() => expect(within(modal).getByText('Test Save Contact')).toBeInTheDocument());
|
||||||
|
expect(within(modal).getByText('(9000000001)')).toBeInTheDocument();
|
||||||
|
// The customer-app accounts are still listed alongside.
|
||||||
|
expect(within(modal).getByText('App Customer')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('picks up a contact saved in another tab without reloading the page', async () => {
|
||||||
|
renderPage();
|
||||||
|
let modal = await openSavedContacts();
|
||||||
|
await waitFor(() => expect(within(modal).getByText('App Customer')).toBeInTheDocument());
|
||||||
|
expect(within(modal).queryByText('Other Tab Contact')).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
// Another tab saves a contact: it writes localStorage, and the browser
|
||||||
|
// fires `storage` in every OTHER tab.
|
||||||
|
act(() => {
|
||||||
|
localStorage.setItem(
|
||||||
|
'doormile_saved_contacts',
|
||||||
|
JSON.stringify([{ id: 'saved_9000000002', name: 'Other Tab Contact', phone: '9000000002', isSavedForLater: true }])
|
||||||
|
);
|
||||||
|
window.dispatchEvent(new StorageEvent('storage', { key: 'doormile_saved_contacts' }));
|
||||||
|
});
|
||||||
|
|
||||||
|
modal = screen.getByRole('heading', { name: 'Saved Contacts' }).closest('div.bg-white');
|
||||||
|
await waitFor(() => expect(within(modal).getByText('Other Tab Contact')).toBeInTheDocument());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lists a number once when it is both saved locally and on the server', async () => {
|
||||||
|
localStorage.setItem(
|
||||||
|
'doormile_saved_contacts',
|
||||||
|
JSON.stringify([{ id: 'saved_9000000001', name: 'Test Save Contact', phone: '9000000001', isSavedForLater: true }])
|
||||||
|
);
|
||||||
|
getTenantCustomers.mockResolvedValue([
|
||||||
|
{ tenantcustomerid: 42, firstname: 'Test', lastname: 'Save Contact', phone: '9000000001' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
renderPage();
|
||||||
|
const modal = await openSavedContacts();
|
||||||
|
await waitFor(() => expect(within(modal).getByText('App Customer')).toBeInTheDocument());
|
||||||
|
expect(within(modal).getAllByText('(9000000001)')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user