Initial commit
This commit is contained in:
137
src/demo/DemoPanel.tsx
Normal file
137
src/demo/DemoPanel.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Demo sign-in — development only.
|
||||
*
|
||||
* Reached solely through a lazy import guarded by `import.meta.env.DEV`, so
|
||||
* this file and `./accounts` are absent from a production build.
|
||||
*/
|
||||
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import { ROLE_LABEL, type ConsoleRole, type SessionUser } from '@/auth/roles';
|
||||
import { DEMO_ACCOUNTS } from './accounts';
|
||||
|
||||
/**
|
||||
* Three one-click sessions, one per role.
|
||||
*
|
||||
* Present only when `import.meta.env.DEV` is true, so a production build drops
|
||||
* this component and the fixtures behind it. It is deliberately styled as a
|
||||
* development tool rather than as part of the form — dashed border, a "DEV
|
||||
* ONLY" tag — because a sign-in shortcut that looks like a real sign-in option
|
||||
* is exactly the thing that ends up in a screenshot for a customer.
|
||||
*/
|
||||
export function DemoPanel({
|
||||
onDemo,
|
||||
}: {
|
||||
onDemo: (role: ConsoleRole, session: SessionUser) => void;
|
||||
}) {
|
||||
const roles: ConsoleRole[] = ['nearle-admin', 'store-admin', 'store-manager'];
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
border: '1px dashed var(--color-border-emphasized, #cbd2db)',
|
||||
borderRadius: 14,
|
||||
padding: 14,
|
||||
background: 'color-mix(in oklab, var(--color-surface-sunken) 55%, transparent)',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
|
||||
<span
|
||||
style={{
|
||||
background: 'var(--color-ink-1)',
|
||||
color: '#fff',
|
||||
borderRadius: 999,
|
||||
padding: '2px 7px',
|
||||
fontSize: 9.5,
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.08em',
|
||||
}}
|
||||
>
|
||||
DEV ONLY
|
||||
</span>
|
||||
<span style={{ fontSize: 12, fontWeight: 600, color: 'var(--color-ink-2)' }}>
|
||||
Sign in without the backend
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p style={{ margin: '0 0 10px', fontSize: 11.5, lineHeight: 1.55, color: 'var(--color-ink-4)' }}>
|
||||
Seeds a session and serves fixtures instead of calling Fiesta. Stripped from production
|
||||
builds.
|
||||
</p>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
{roles.map((role) => {
|
||||
const account = DEMO_ACCOUNTS[role];
|
||||
if (!account) return null;
|
||||
return (
|
||||
<button
|
||||
key={role}
|
||||
type="button"
|
||||
onClick={() => onDemo(role, account.session)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
width: '100%',
|
||||
textAlign: 'left',
|
||||
padding: '9px 11px',
|
||||
borderRadius: 12,
|
||||
border: '1px solid var(--color-line)',
|
||||
background: 'var(--color-surface)',
|
||||
cursor: 'pointer',
|
||||
transition: 'border-color .2s, background .2s',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
width: 26,
|
||||
height: 26,
|
||||
borderRadius: 8,
|
||||
background: 'var(--color-brand-tint)',
|
||||
color: 'var(--color-brand)',
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
fontSize: 11,
|
||||
fontWeight: 700,
|
||||
flex: 'none',
|
||||
}}
|
||||
>
|
||||
{account.session.name
|
||||
.split(' ')
|
||||
.map((part) => part[0])
|
||||
.slice(0, 2)
|
||||
.join('')}
|
||||
</span>
|
||||
<span style={{ minWidth: 0, flex: 1 }}>
|
||||
<span
|
||||
style={{
|
||||
display: 'block',
|
||||
fontSize: 12.5,
|
||||
fontWeight: 600,
|
||||
color: 'var(--color-ink-1)',
|
||||
}}
|
||||
>
|
||||
{ROLE_LABEL[role]}
|
||||
<span style={{ fontWeight: 400, color: 'var(--color-ink-4)' }}>
|
||||
{' '}
|
||||
· {account.standsFor}
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
display: 'block',
|
||||
fontSize: 11,
|
||||
lineHeight: 1.45,
|
||||
color: 'var(--color-ink-3)',
|
||||
}}
|
||||
>
|
||||
{account.blurb}
|
||||
</span>
|
||||
</span>
|
||||
<ArrowRight size={14} style={{ color: 'var(--color-ink-4)', flex: 'none' }} />
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
69
src/demo/accounts.ts
Normal file
69
src/demo/accounts.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* The three demo accounts.
|
||||
*
|
||||
* Split from `src/demo/index.ts` so they can be reached only through the lazily
|
||||
* loaded demo panel — a static import would put these session objects in the
|
||||
* production bundle, and a build that ships fake accounts is a build that
|
||||
* eventually ships one to a customer.
|
||||
*
|
||||
* DEV ONLY.
|
||||
*/
|
||||
|
||||
import type { SessionUser } from '@/auth/roles';
|
||||
|
||||
export interface DemoAccount {
|
||||
session: SessionUser;
|
||||
/** What this role is for, in one line, shown on the button. */
|
||||
blurb: string;
|
||||
/** The real login this stands in for, so it is obvious what it maps to. */
|
||||
standsFor: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* One per role, matching how `resolveRole` actually decides:
|
||||
* `issuperadmin` first, then roleid 1 or 3, then everything else.
|
||||
*/
|
||||
export const DEMO_ACCOUNTS: Record<string, DemoAccount> = {
|
||||
'nearle-admin': {
|
||||
session: {
|
||||
userid: 9001,
|
||||
role: 'nearle-admin',
|
||||
name: 'Suriya Kumar',
|
||||
email: 'suriya@superadmin.com',
|
||||
roleid: 0,
|
||||
tenantid: 0,
|
||||
locationid: 0,
|
||||
issuperadmin: true,
|
||||
},
|
||||
blurb: 'Onboard tenants and branches, browse the global catalogue, see every store.',
|
||||
standsFor: 'issuperadmin = true',
|
||||
},
|
||||
'store-admin': {
|
||||
session: {
|
||||
userid: 9002,
|
||||
role: 'store-admin',
|
||||
name: 'Rmart Operations',
|
||||
email: 'rmart@gmail.com',
|
||||
roleid: 1,
|
||||
tenantid: 1135,
|
||||
locationid: 1,
|
||||
issuperadmin: false,
|
||||
},
|
||||
blurb: 'One merchant group — its branches, catalogue, reports and tills.',
|
||||
standsFor: 'roleid 1',
|
||||
},
|
||||
'store-manager': {
|
||||
session: {
|
||||
userid: 9003,
|
||||
role: 'store-manager',
|
||||
name: 'R Mart User',
|
||||
email: 'rmartuser@gmail.com',
|
||||
roleid: 5,
|
||||
tenantid: 1135,
|
||||
locationid: 1,
|
||||
issuperadmin: false,
|
||||
},
|
||||
blurb: 'A single store — its orders, stock, customers and counter sales.',
|
||||
standsFor: 'any other roleid',
|
||||
},
|
||||
};
|
||||
961
src/demo/fixtures.ts
Normal file
961
src/demo/fixtures.ts
Normal file
@@ -0,0 +1,961 @@
|
||||
/**
|
||||
* Demo fixtures.
|
||||
*
|
||||
* Shapes and values are copied from the live tenant so the screens read as the
|
||||
* real thing rather than as lorem ipsum: R mart in RS Puram, the Aachi/Amul/
|
||||
* Cadbury catalogue, the 97.6% cancel rate. Anyone who knows the data will
|
||||
* recognise it, which is the point — a demo that looks nothing like production
|
||||
* teaches you nothing about production.
|
||||
*
|
||||
* DEV ONLY. See `src/demo/index.ts` for how this is gated and stripped.
|
||||
*/
|
||||
|
||||
import type {
|
||||
CatalogueBrand,
|
||||
CatalogueProduct,
|
||||
CatalogueRef,
|
||||
LocationOrderSummary,
|
||||
OrderSummary,
|
||||
Product,
|
||||
ProductCategory,
|
||||
TenantInfo,
|
||||
TenantLocation,
|
||||
} from '@/api/types';
|
||||
|
||||
export const DEMO_TENANTS: TenantInfo[] = [
|
||||
{
|
||||
tenantid: 1135,
|
||||
locationid: 1,
|
||||
tenantname: 'R mart',
|
||||
locationname: 'RS Puram',
|
||||
companyname: 'R Mart Retail Pvt. Ltd.',
|
||||
primaryemail: 'rmart@gmail.com',
|
||||
primarycontact: '9879879876',
|
||||
city: 'Coimbatore',
|
||||
state: 'Tamil Nadu',
|
||||
suburb: 'RS Puram',
|
||||
postcode: '641002',
|
||||
status: 'Active',
|
||||
},
|
||||
{
|
||||
tenantid: 1136,
|
||||
locationid: 4,
|
||||
tenantname: 'Kaveri Groceries',
|
||||
locationname: 'Peelamedu',
|
||||
companyname: 'Kaveri Retail Pvt. Ltd.',
|
||||
primaryemail: 'admin@kaveri.com',
|
||||
primarycontact: '9876543210',
|
||||
city: 'Coimbatore',
|
||||
state: 'Tamil Nadu',
|
||||
suburb: 'Peelamedu',
|
||||
postcode: '641004',
|
||||
status: 'Active',
|
||||
},
|
||||
{
|
||||
tenantid: 1136,
|
||||
locationid: 5,
|
||||
tenantname: 'Kaveri Groceries',
|
||||
locationname: 'Saibaba Colony',
|
||||
companyname: 'Kaveri Retail Pvt. Ltd.',
|
||||
primaryemail: 'admin@kaveri.com',
|
||||
primarycontact: '9876543210',
|
||||
city: 'Coimbatore',
|
||||
state: 'Tamil Nadu',
|
||||
status: 'Active',
|
||||
},
|
||||
{
|
||||
tenantid: 1137,
|
||||
locationid: 8,
|
||||
tenantname: 'Anbu Stores',
|
||||
locationname: 'Gandhipuram',
|
||||
companyname: 'Anbu Traders',
|
||||
primaryemail: 'andiraj@gmail.com',
|
||||
primarycontact: '7397177923',
|
||||
city: 'Coimbatore',
|
||||
state: 'Tamil Nadu',
|
||||
status: 'InActive',
|
||||
},
|
||||
];
|
||||
|
||||
export const DEMO_LOCATIONS: Record<number, TenantLocation[]> = {
|
||||
1135: [
|
||||
{
|
||||
locationid: 1,
|
||||
tenantid: 1135,
|
||||
locationname: 'R mart — RS Puram',
|
||||
email: 'rspuram@rmart.com',
|
||||
contactno: '9879879876',
|
||||
address: '240, DB Road',
|
||||
suburb: 'RS Puram',
|
||||
city: 'Coimbatore',
|
||||
state: 'Tamil Nadu',
|
||||
postcode: '641002',
|
||||
opentime: '08:00',
|
||||
closetime: '22:00',
|
||||
deliveryradius: 5000,
|
||||
deliverymins: 30,
|
||||
status: 'Active',
|
||||
},
|
||||
{
|
||||
locationid: 2,
|
||||
tenantid: 1135,
|
||||
locationname: 'R mart — Peelamedu',
|
||||
email: 'peelamedu@rmart.com',
|
||||
contactno: '9879879877',
|
||||
address: '18, Avinashi Road',
|
||||
suburb: 'Peelamedu',
|
||||
city: 'Coimbatore',
|
||||
state: 'Tamil Nadu',
|
||||
postcode: '641004',
|
||||
opentime: '07:00',
|
||||
closetime: '23:00',
|
||||
deliveryradius: 6000,
|
||||
deliverymins: 25,
|
||||
status: 'Active',
|
||||
},
|
||||
{
|
||||
locationid: 3,
|
||||
tenantid: 1135,
|
||||
locationname: 'R mart — Saravanampatti',
|
||||
email: 'saravanampatti@rmart.com',
|
||||
contactno: '9879879878',
|
||||
address: '5, Sathy Main Road',
|
||||
suburb: 'Saravanampatti',
|
||||
city: 'Coimbatore',
|
||||
state: 'Tamil Nadu',
|
||||
postcode: '641035',
|
||||
opentime: '08:00',
|
||||
closetime: '22:00',
|
||||
deliveryradius: 4500,
|
||||
deliverymins: 35,
|
||||
status: 'Active',
|
||||
},
|
||||
],
|
||||
1136: [
|
||||
{
|
||||
locationid: 4,
|
||||
tenantid: 1136,
|
||||
locationname: 'Kaveri — Peelamedu',
|
||||
address: '12, Avinashi Road',
|
||||
suburb: 'Peelamedu',
|
||||
city: 'Coimbatore',
|
||||
state: 'Tamil Nadu',
|
||||
postcode: '641004',
|
||||
opentime: '07:00',
|
||||
closetime: '23:00',
|
||||
deliveryradius: 7000,
|
||||
deliverymins: 25,
|
||||
status: 'Active',
|
||||
},
|
||||
{
|
||||
locationid: 5,
|
||||
tenantid: 1136,
|
||||
locationname: 'Kaveri — Saibaba Colony',
|
||||
address: '88, Mettupalayam Road',
|
||||
suburb: 'Saibaba Colony',
|
||||
city: 'Coimbatore',
|
||||
state: 'Tamil Nadu',
|
||||
postcode: '641011',
|
||||
opentime: '08:00',
|
||||
closetime: '22:00',
|
||||
deliveryradius: 4000,
|
||||
deliverymins: 35,
|
||||
status: 'Active',
|
||||
},
|
||||
],
|
||||
1137: [
|
||||
{
|
||||
locationid: 8,
|
||||
tenantid: 1137,
|
||||
locationname: 'Anbu — Gandhipuram',
|
||||
address: '5, Cross Cut Road',
|
||||
city: 'Coimbatore',
|
||||
state: 'Tamil Nadu',
|
||||
postcode: '641012',
|
||||
opentime: '09:00',
|
||||
closetime: '21:00',
|
||||
deliveryradius: 3000,
|
||||
deliverymins: 40,
|
||||
status: 'InActive',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/** Per-branch order totals — the shape `/orders/getlocationsummary` returns. */
|
||||
export const DEMO_LOCATION_SUMMARY: Record<number, LocationOrderSummary[]> = {
|
||||
1135: [
|
||||
{ locationid: 1, locationname: 'R mart — RS Puram', totalorders: 84, delivered: 1, cancelled: 82, revenue: 1350 },
|
||||
{ locationid: 2, locationname: 'R mart — Peelamedu', totalorders: 268, delivered: 241, cancelled: 19, revenue: 58400 },
|
||||
{ locationid: 3, locationname: 'R mart — Saravanampatti', totalorders: 142, delivered: 126, cancelled: 12, revenue: 27950 },
|
||||
],
|
||||
1136: [
|
||||
{ locationid: 4, locationname: 'Kaveri — Peelamedu', totalorders: 412, delivered: 388, cancelled: 11, revenue: 84250 },
|
||||
{ locationid: 5, locationname: 'Kaveri — Saibaba Colony', totalorders: 196, delivered: 181, cancelled: 6, revenue: 39800 },
|
||||
],
|
||||
1137: [{ locationid: 8, locationname: 'Anbu — Gandhipuram', totalorders: 0, delivered: 0, cancelled: 0, revenue: 0 }],
|
||||
};
|
||||
|
||||
export const DEMO_ORDER_SUMMARY: Record<number, OrderSummary> = {
|
||||
1135: { totalorders: 84, delivered: 1, pending: 0, cancelled: 82, revenue: 1350 },
|
||||
1136: { totalorders: 608, delivered: 569, pending: 22, cancelled: 17, revenue: 124050 },
|
||||
1137: { totalorders: 0, delivered: 0, pending: 0, cancelled: 0, revenue: 0 },
|
||||
};
|
||||
|
||||
export const DEMO_BRANDS: CatalogueBrand[] = [
|
||||
{ brand: 'aachi', product_count: 176 },
|
||||
{ brand: 'amul', product_count: 122 },
|
||||
{ brand: 'anil', product_count: 50 },
|
||||
{ brand: 'cadbury', product_count: 110 },
|
||||
{ brand: 'cavinkare', product_count: 45 },
|
||||
{ brand: 'nestle', product_count: 123 },
|
||||
{ brand: 'pepsico', product_count: 88 },
|
||||
];
|
||||
|
||||
function catalogueProduct(
|
||||
id: number,
|
||||
brand: string,
|
||||
name: string,
|
||||
size: string,
|
||||
range: string,
|
||||
category: string,
|
||||
): CatalogueProduct {
|
||||
return {
|
||||
id,
|
||||
brand,
|
||||
product_name: name,
|
||||
size,
|
||||
price_range: range,
|
||||
category,
|
||||
product_sku: `${brand.toUpperCase()}-${id}`,
|
||||
};
|
||||
}
|
||||
|
||||
export const DEMO_CATALOGUE: CatalogueProduct[] = [
|
||||
catalogueProduct(43, 'aachi', 'Aachi Appalams 100g', '100g', '₹33-37', 'Snacks'),
|
||||
catalogueProduct(89, 'aachi', 'Aachi Chilli Powder 100gm', '100gm', '₹31-39', 'Masala'),
|
||||
catalogueProduct(7, 'aachi', 'Aachi Chicken Masala 100g', '100g', '₹23-27', 'Masala'),
|
||||
catalogueProduct(20, 'aachi', 'Aachi Garam Masala 100g', '100g', '₹22-28', 'Masala'),
|
||||
catalogueProduct(102, 'aachi', 'Aachi Sambar Powder 50g', '50g', '₹14-16', 'Masala'),
|
||||
catalogueProduct(51, 'amul', 'Amul Milk 1L', '1L', '₹68-92', 'Dairy'),
|
||||
catalogueProduct(3, 'amul', 'Amul Butter 500ml', '500ml', '₹80-100', 'Dairy'),
|
||||
catalogueProduct(37, 'amul', 'Amul Dahi 200 g', '200 g', '₹28-32', 'Dairy'),
|
||||
catalogueProduct(20, 'amul', 'Amul Ghee 1L', '1L', '₹158-192', 'Dairy'),
|
||||
catalogueProduct(77, 'amul', 'Amul 150 g', '150 g', '₹37-43', 'Dairy'),
|
||||
catalogueProduct(9, 'cadbury', 'Cadbury Bournvita 500g', '500g', '₹210-230', 'Beverages'),
|
||||
catalogueProduct(12, 'cadbury', 'Cadbury Dairy Milk 100g', '100g', '₹77-93', 'Confectionery'),
|
||||
catalogueProduct(37, 'cadbury', 'Cadbury Oreo 100g', '100g', '₹10-14', 'Biscuits'),
|
||||
catalogueProduct(15, 'cadbury', 'Cadbury Gems 25g', '25g', '₹22-28', 'Confectionery'),
|
||||
catalogueProduct(10, 'anil', 'Anil Wheat Flour 1kg', '1kg', '₹251-299', 'Staples'),
|
||||
catalogueProduct(34, 'anil', 'Anil Idli Dosa Mix 500g', '500g', '₹98-122', 'Staples'),
|
||||
catalogueProduct(1, 'cavinkare', 'Cavinkare Frooti 200g', '200g', '₹42-48', 'Beverages'),
|
||||
catalogueProduct(30, 'cavinkare', 'Cavinkare Chik Shampoo 500ml', '500ml', '₹377-463', 'Personal care'),
|
||||
];
|
||||
|
||||
/** `(brand, catalogueid)` pairs the demo tenant has already imported. */
|
||||
export const DEMO_IMPORTED: Record<number, CatalogueRef[]> = {
|
||||
1135: [
|
||||
{ brand: 'cadbury', catalogueid: 9 },
|
||||
{ brand: 'amul', catalogueid: 77 },
|
||||
{ brand: 'amul', catalogueid: 51 },
|
||||
],
|
||||
1136: [{ brand: 'aachi', catalogueid: 89 }],
|
||||
1137: [],
|
||||
};
|
||||
|
||||
export const DEMO_CATEGORIES: ProductCategory[] = [
|
||||
{ categoryid: 1, categoryname: 'Grocery & Staples' },
|
||||
{ categoryid: 2, categoryname: 'Dairy & Chilled' },
|
||||
{ categoryid: 3, categoryname: 'Snacks & Beverages' },
|
||||
{ categoryid: 4, categoryname: 'Personal Care' },
|
||||
{ categoryid: 5, categoryname: 'Household' },
|
||||
];
|
||||
|
||||
export const DEMO_STORE_PRODUCTS: Product[] = [
|
||||
{
|
||||
productid: 7081,
|
||||
productname: 'Cadbury Bournvita 500g',
|
||||
productsku: 'B06XSB6RB2',
|
||||
productunit: '500g',
|
||||
retailprice: 100,
|
||||
price: 100,
|
||||
productstock: 0,
|
||||
productstatus: 'Active',
|
||||
categoryname: 'Snacks & Beverages',
|
||||
},
|
||||
{
|
||||
productid: 7077,
|
||||
productname: 'Hot Heads 30g',
|
||||
productsku: 'NESTLE-HOT-30-001',
|
||||
productunit: '30g',
|
||||
retailprice: 50,
|
||||
price: 50,
|
||||
productstock: 47,
|
||||
productstatus: 'Active',
|
||||
categoryname: 'Snacks & Beverages',
|
||||
},
|
||||
{
|
||||
productid: 7076,
|
||||
productname: 'Cheerios Breakfast Cereal 100g',
|
||||
productsku: 'B08X68PTVG',
|
||||
productunit: '100g',
|
||||
retailprice: 120,
|
||||
price: 120,
|
||||
productstock: 17,
|
||||
productstatus: 'Active',
|
||||
categoryname: 'Grocery & Staples',
|
||||
},
|
||||
];
|
||||
|
||||
/* ────────────────────────────────────────────────────────────────────────────
|
||||
Store Admin — POS, stock and the approval queue
|
||||
|
||||
The three R Mart branches are deliberately in three different states, because
|
||||
a demo where everything is green teaches nothing about the screen:
|
||||
|
||||
- RS Puram one till, synced, trading normally
|
||||
- Peelamedu two tills, one of them syncing and one stranded for an hour
|
||||
- Saravanampatti till offline entirely, with takings queued behind it
|
||||
──────────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
import type { PosSalesSummary, PosTerminalHealth, StockRequest, StockStatementRow } from '@/api/types';
|
||||
|
||||
/** Minutes ago, as the ISO string a till would have reported. */
|
||||
const ago = (minutes: number): string => new Date(Date.now() - minutes * 60_000).toISOString();
|
||||
|
||||
export const DEMO_POS_HEALTH: Record<number, PosTerminalHealth[]> = {
|
||||
1: [
|
||||
{
|
||||
terminal_id: 'RSP-01',
|
||||
location_id: '1',
|
||||
store_name: 'R mart — RS Puram',
|
||||
app_version: '2.4.1',
|
||||
status: 'online',
|
||||
pending_bills: '0',
|
||||
pending_registrations: '0',
|
||||
today_bills: '46',
|
||||
today_amount: '18240',
|
||||
last_bill_at: ago(3),
|
||||
reported_at: ago(0),
|
||||
received_at: ago(0),
|
||||
printer_reachable: 'true',
|
||||
storage_free_mb: '2400',
|
||||
battery_level: '86',
|
||||
battery_charging: 'true',
|
||||
drawer_status: 'closed',
|
||||
},
|
||||
{
|
||||
// The regression case, made visible: still claiming "online", but nothing
|
||||
// has arrived in six minutes. The old derivation badged this "Synced".
|
||||
terminal_id: 'RSP-02',
|
||||
location_id: '1',
|
||||
store_name: 'R mart — RS Puram',
|
||||
app_version: '2.4.1',
|
||||
status: 'online',
|
||||
pending_bills: '0',
|
||||
pending_registrations: '0',
|
||||
today_bills: '31',
|
||||
today_amount: '11405',
|
||||
last_bill_at: ago(7),
|
||||
reported_at: ago(6),
|
||||
received_at: ago(6),
|
||||
printer_reachable: 'true',
|
||||
storage_free_mb: '2100',
|
||||
},
|
||||
{
|
||||
// Commissioned, heartbeating, never put into service.
|
||||
terminal_id: 'RSP-03',
|
||||
location_id: '1',
|
||||
store_name: 'R mart — RS Puram',
|
||||
app_version: '2.4.1',
|
||||
status: 'online',
|
||||
pending_bills: '0',
|
||||
pending_registrations: '0',
|
||||
today_bills: '0',
|
||||
today_amount: '0',
|
||||
reported_at: ago(0),
|
||||
received_at: ago(0),
|
||||
printer_reachable: 'true',
|
||||
storage_free_mb: '3100',
|
||||
},
|
||||
],
|
||||
2: [
|
||||
{
|
||||
terminal_id: 'PLM-01',
|
||||
location_id: '2',
|
||||
store_name: 'R mart — Peelamedu',
|
||||
app_version: '2.4.1',
|
||||
status: 'online',
|
||||
pending_bills: '3',
|
||||
pending_registrations: '0',
|
||||
oldest_pending_at: ago(2),
|
||||
today_bills: '112',
|
||||
today_amount: '46310',
|
||||
last_bill_at: ago(1),
|
||||
// Reporting a time nine minutes ahead of when we received it — the till's
|
||||
// clock is wrong, and its bills carry the wrong business date.
|
||||
reported_at: ago(-9),
|
||||
received_at: ago(0),
|
||||
printer_reachable: 'true',
|
||||
storage_free_mb: '1850',
|
||||
},
|
||||
{
|
||||
terminal_id: 'PLM-02',
|
||||
location_id: '2',
|
||||
store_name: 'R mart — Peelamedu',
|
||||
app_version: '2.3.9',
|
||||
status: 'online',
|
||||
pending_bills: '27',
|
||||
pending_registrations: '2',
|
||||
oldest_pending_at: ago(64),
|
||||
today_bills: '58',
|
||||
today_amount: '21470',
|
||||
last_bill_at: ago(64),
|
||||
reported_at: ago(0),
|
||||
received_at: ago(0),
|
||||
printer_reachable: 'false',
|
||||
storage_free_mb: '140',
|
||||
battery_level: '14',
|
||||
battery_charging: 'false',
|
||||
},
|
||||
],
|
||||
3: [
|
||||
{
|
||||
terminal_id: 'SRV-01',
|
||||
location_id: '3',
|
||||
store_name: 'R mart — Saravanampatti',
|
||||
app_version: '2.3.9',
|
||||
status: 'offline',
|
||||
pending_bills: '41',
|
||||
pending_registrations: '0',
|
||||
oldest_pending_at: ago(196),
|
||||
today_bills: '39',
|
||||
today_amount: '15880',
|
||||
last_bill_at: ago(196),
|
||||
reported_at: ago(194),
|
||||
received_at: ago(194),
|
||||
drawer_status: 'open',
|
||||
},
|
||||
{
|
||||
// The expired-key stub: terminal, location, status, reason — and nothing
|
||||
// else. No heartbeat fields at all, which is the whole tell.
|
||||
terminal_id: 'SRV-02',
|
||||
location_id: '3',
|
||||
status: 'offline',
|
||||
reason: 'presence key expired',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const DEMO_POS_SUMMARY: Record<number, PosSalesSummary> = {
|
||||
1: {
|
||||
locationid: 1,
|
||||
billcount: 46,
|
||||
itemcount: 214,
|
||||
grosssales: 18240,
|
||||
taxcollected: 1640,
|
||||
discountgiven: 380,
|
||||
roundoff: 12,
|
||||
averagebill: 396.5,
|
||||
bypaymentmode: [
|
||||
{ paymentmode: 'UPI', amount: 11420, billcount: 31 },
|
||||
{ paymentmode: 'Cash', amount: 5380, billcount: 12 },
|
||||
{ paymentmode: 'Card', amount: 1440, billcount: 3 },
|
||||
],
|
||||
byterminal: [
|
||||
{ terminalid: 'RSP-01', amount: 18240, billcount: 46 },
|
||||
{ terminalid: 'RSP-02', amount: 11405, billcount: 31 },
|
||||
// Billing, with no health record anywhere. Invisible on the board until
|
||||
// the sales split is reconciled against it — which is the point.
|
||||
{ terminalid: 'RSP-04', amount: 6820, billcount: 17 },
|
||||
],
|
||||
byday: [
|
||||
{ day: '2026-08-17', amount: 15900, billcount: 41 },
|
||||
{ day: '2026-08-18', amount: 17210, billcount: 44 },
|
||||
{ day: '2026-08-19', amount: 16480, billcount: 42 },
|
||||
{ day: '2026-08-20', amount: 19340, billcount: 49 },
|
||||
{ day: '2026-08-21', amount: 18240, billcount: 46 },
|
||||
],
|
||||
},
|
||||
2: {
|
||||
locationid: 2,
|
||||
billcount: 170,
|
||||
itemcount: 806,
|
||||
grosssales: 67780,
|
||||
taxcollected: 6120,
|
||||
discountgiven: 1450,
|
||||
roundoff: 38,
|
||||
averagebill: 398.7,
|
||||
bypaymentmode: [
|
||||
{ paymentmode: 'UPI', amount: 41260, billcount: 104 },
|
||||
{ paymentmode: 'Cash', amount: 18940, billcount: 52 },
|
||||
{ paymentmode: 'Card', amount: 7580, billcount: 14 },
|
||||
],
|
||||
byterminal: [
|
||||
{ terminalid: 'PLM-01', amount: 46310, billcount: 112 },
|
||||
{ terminalid: 'PLM-02', amount: 21470, billcount: 58 },
|
||||
],
|
||||
byday: [
|
||||
{ day: '2026-08-17', amount: 61200, billcount: 158 },
|
||||
{ day: '2026-08-18', amount: 64850, billcount: 163 },
|
||||
{ day: '2026-08-19', amount: 59900, billcount: 151 },
|
||||
{ day: '2026-08-20', amount: 71340, billcount: 178 },
|
||||
{ day: '2026-08-21', amount: 67780, billcount: 170 },
|
||||
],
|
||||
},
|
||||
3: {
|
||||
locationid: 3,
|
||||
billcount: 39,
|
||||
itemcount: 168,
|
||||
grosssales: 15880,
|
||||
taxcollected: 1430,
|
||||
discountgiven: 210,
|
||||
roundoff: 7,
|
||||
averagebill: 407.2,
|
||||
bypaymentmode: [
|
||||
{ paymentmode: 'Cash', amount: 9640, billcount: 26 },
|
||||
{ paymentmode: 'UPI', amount: 6240, billcount: 13 },
|
||||
],
|
||||
byterminal: [{ terminalid: 'SRV-01', amount: 15880, billcount: 39 }],
|
||||
byday: [
|
||||
{ day: '2026-08-19', amount: 17420, billcount: 44 },
|
||||
{ day: '2026-08-20', amount: 16900, billcount: 43 },
|
||||
{ day: '2026-08-21', amount: 15880, billcount: 39 },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const DEMO_STOCK_REQUESTS: StockRequest[] = [
|
||||
{
|
||||
requestid: 501,
|
||||
tenantid: 1135,
|
||||
locationid: 2,
|
||||
locationname: 'R mart — Peelamedu',
|
||||
productid: 9001,
|
||||
productname: 'Aachi Chilli Powder 100gm',
|
||||
qty: 60,
|
||||
status: 'Pending',
|
||||
created: ago(95),
|
||||
},
|
||||
{
|
||||
requestid: 502,
|
||||
tenantid: 1135,
|
||||
locationid: 3,
|
||||
locationname: 'R mart — Saravanampatti',
|
||||
productid: 9004,
|
||||
productname: 'Amul Butter 500g',
|
||||
qty: 24,
|
||||
status: 'Pending',
|
||||
created: ago(240),
|
||||
},
|
||||
{
|
||||
requestid: 503,
|
||||
tenantid: 1135,
|
||||
locationid: 1,
|
||||
locationname: 'R mart — RS Puram',
|
||||
productid: 9007,
|
||||
productname: 'Cadbury Dairy Milk 50g',
|
||||
qty: 100,
|
||||
status: 'Pending',
|
||||
created: ago(410),
|
||||
},
|
||||
{
|
||||
requestid: 498,
|
||||
tenantid: 1135,
|
||||
locationid: 2,
|
||||
locationname: 'R mart — Peelamedu',
|
||||
productid: 9002,
|
||||
productname: 'Aachi Garam Masala 100g',
|
||||
qty: 40,
|
||||
status: 'Received',
|
||||
created: ago(1500),
|
||||
},
|
||||
{
|
||||
requestid: 497,
|
||||
tenantid: 1135,
|
||||
locationid: 1,
|
||||
locationname: 'R mart — RS Puram',
|
||||
productid: 9005,
|
||||
productname: 'Amul Cheese Slices 200g',
|
||||
qty: 15,
|
||||
status: 'Rejected',
|
||||
created: ago(2100),
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* A stock statement per branch.
|
||||
*
|
||||
* `debit` is what left the shelf — both channels together, which is why it is
|
||||
* the figure the fast/slow-mover lists rank on rather than either sales table.
|
||||
*/
|
||||
export const DEMO_STOCK_STATEMENT: Record<number, StockStatementRow[]> = {
|
||||
1: [
|
||||
{ productid: 9001, productname: 'Aachi Chilli Powder 100gm', opening: 80, credit: 40, debit: 96, closing: 24, retailprice: 35, locationid: 1 },
|
||||
{ productid: 9004, productname: 'Amul Butter 500g', opening: 30, credit: 24, debit: 52, closing: 2, retailprice: 265, locationid: 1 },
|
||||
{ productid: 9007, productname: 'Cadbury Dairy Milk 50g', opening: 120, credit: 0, debit: 120, closing: 0, retailprice: 45, locationid: 1 },
|
||||
{ productid: 9002, productname: 'Aachi Garam Masala 100g', opening: 60, credit: 20, debit: 31, closing: 49, retailprice: 42, locationid: 1 },
|
||||
{ productid: 9005, productname: 'Amul Cheese Slices 200g', opening: 25, credit: 10, debit: 8, closing: 27, retailprice: 145, locationid: 1 },
|
||||
{ productid: 9009, productname: 'Nestlé Munch 32g', opening: 200, credit: 0, debit: 14, closing: 186, retailprice: 20, locationid: 1 },
|
||||
],
|
||||
2: [
|
||||
{ productid: 9001, productname: 'Aachi Chilli Powder 100gm', opening: 140, credit: 60, debit: 196, closing: 4, retailprice: 35, locationid: 2 },
|
||||
{ productid: 9004, productname: 'Amul Butter 500g', opening: 60, credit: 48, debit: 91, closing: 17, retailprice: 265, locationid: 2 },
|
||||
{ productid: 9007, productname: 'Cadbury Dairy Milk 50g', opening: 220, credit: 100, debit: 288, closing: 32, retailprice: 45, locationid: 2 },
|
||||
{ productid: 9009, productname: 'Nestlé Munch 32g', opening: 300, credit: 0, debit: 41, closing: 259, retailprice: 20, locationid: 2 },
|
||||
],
|
||||
3: [
|
||||
{ productid: 9004, productname: 'Amul Butter 500g', opening: 20, credit: 0, debit: 20, closing: 0, retailprice: 265, locationid: 3 },
|
||||
{ productid: 9001, productname: 'Aachi Chilli Powder 100gm', opening: 70, credit: 30, debit: 88, closing: 12, retailprice: 35, locationid: 3 },
|
||||
{ productid: 9009, productname: 'Nestlé Munch 32g', opening: 150, credit: 0, debit: 9, closing: 141, retailprice: 20, locationid: 3 },
|
||||
],
|
||||
};
|
||||
|
||||
/* ────────────────────────────────────────────────────────────────────────────
|
||||
Sales rows — app orders, deliveries and counter bills
|
||||
|
||||
The order set mirrors the shape of the live tenant rather than an idealised
|
||||
one: RS Puram's cancel rate is genuinely 98%, so most of its orders are
|
||||
cancelled here too. A demo that only shows the happy path hides the exact
|
||||
thing this page exists to surface.
|
||||
──────────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
import type { DeliveryRow, OrderRow, PosSale } from '@/api/types';
|
||||
|
||||
/** Hours ago, as the timestamp string the API returns. */
|
||||
const hoursAgo = (h: number): string =>
|
||||
new Date(Date.now() - h * 3_600_000).toISOString().slice(0, 19).replace('T', ' ');
|
||||
|
||||
const BRANCH_NAME: Record<number, string> = {
|
||||
1: 'R mart — RS Puram',
|
||||
2: 'R mart — Peelamedu',
|
||||
3: 'R mart — Saravanampatti',
|
||||
};
|
||||
|
||||
const CUSTOMERS = [
|
||||
{ name: 'Meena Ravi', phone: '9840012233', suburb: 'RS Puram', address: '12, Bharathi Street, RS Puram' },
|
||||
{ name: 'Karthik S', phone: '9840044556', suburb: 'Peelamedu', address: '7, Nehru Nagar, Peelamedu' },
|
||||
{ name: 'Divya Prakash', phone: '9840077889', suburb: 'Saravanampatti', address: '22, Sathy Road, Saravanampatti' },
|
||||
{ name: 'Arun Kumar', phone: '9840099001', suburb: 'Gandhipuram', address: '90, Cross Cut Road, Gandhipuram' },
|
||||
{ name: 'Lakshmi N', phone: '9840033445', suburb: 'Peelamedu', address: '3, Avinashi Road, Peelamedu' },
|
||||
];
|
||||
|
||||
const RIDERS = ['Suresh M', 'Vignesh R', 'Prakash T'];
|
||||
|
||||
function order(
|
||||
id: number,
|
||||
locationid: number,
|
||||
hours: number,
|
||||
orderstatus: string,
|
||||
deliverystatus: string,
|
||||
amount: number,
|
||||
items: number,
|
||||
riderIndex: number | null,
|
||||
): OrderRow {
|
||||
const customer = CUSTOMERS[id % CUSTOMERS.length] as (typeof CUSTOMERS)[number];
|
||||
const isDone = deliverystatus === 'Delivered';
|
||||
const isMoving = deliverystatus === 'Out for Delivery' || isDone;
|
||||
return {
|
||||
orderheaderid: 7000 + id,
|
||||
orderid: `RM${String(24000 + id)}`,
|
||||
tenantid: 1135,
|
||||
locationid,
|
||||
locationname: BRANCH_NAME[locationid],
|
||||
orderdate: hoursAgo(hours),
|
||||
orderstatus,
|
||||
deliverystatus,
|
||||
itemcount: items,
|
||||
orderamount: amount,
|
||||
deliverycharge: 25,
|
||||
deliverycustomer: customer.name,
|
||||
deliverycontactno: customer.phone,
|
||||
deliveryaddress: customer.address,
|
||||
deliverysuburb: customer.suburb,
|
||||
pickupcustomer: BRANCH_NAME[locationid],
|
||||
pickupsuburb: customer.suburb,
|
||||
deliveryid: deliverystatus ? 5000 + id : 0,
|
||||
rider: riderIndex === null ? '' : RIDERS[riderIndex],
|
||||
ridercontactno: riderIndex === null ? '' : '9000011122',
|
||||
riderkms: isMoving ? (2 + (id % 5)).toFixed(1) : '',
|
||||
assigntime: riderIndex === null ? '' : hoursAgo(hours - 0.2),
|
||||
pickuptime: isMoving ? hoursAgo(hours - 0.4) : '',
|
||||
deliverytime: isDone ? hoursAgo(hours - 0.9) : '',
|
||||
canceltime: orderstatus === 'Cancelled' ? hoursAgo(hours - 0.3) : '',
|
||||
};
|
||||
}
|
||||
|
||||
export const DEMO_ORDERS: OrderRow[] = [
|
||||
// Peelamedu — the branch that actually trades well.
|
||||
order(1, 2, 0.4, 'Confirmed', 'Out for Delivery', 862, 9, 0),
|
||||
order(2, 2, 1.1, 'Delivered', 'Delivered', 1240, 14, 1),
|
||||
order(3, 2, 2.3, 'Delivered', 'Delivered', 415, 5, 0),
|
||||
order(4, 2, 3.0, 'Preparing', 'Pending', 690, 7, null),
|
||||
order(5, 2, 4.6, 'Delivered', 'Delivered', 2180, 22, 2),
|
||||
order(6, 2, 6.2, 'Cancelled', 'Cancelled', 320, 4, null),
|
||||
order(7, 2, 8.0, 'Delivered', 'Delivered', 975, 11, 1),
|
||||
// Saravanampatti — steady, a couple in flight.
|
||||
order(8, 3, 0.8, 'Ready', 'Pending', 540, 6, null),
|
||||
order(9, 3, 2.0, 'Confirmed', 'Out for Delivery', 1105, 12, 2),
|
||||
order(10, 3, 5.4, 'Delivered', 'Delivered', 780, 8, 0),
|
||||
order(11, 3, 9.1, 'Cancelled', 'Cancelled', 260, 3, null),
|
||||
// RS Puram — the 98% cancel rate, which is the finding worth surfacing.
|
||||
order(12, 1, 1.5, 'Cancelled', 'Cancelled', 180, 2, null),
|
||||
order(13, 1, 3.7, 'Cancelled', 'Cancelled', 340, 4, null),
|
||||
order(14, 1, 5.9, 'Cancelled', 'Cancelled', 95, 1, null),
|
||||
order(15, 1, 7.2, 'Cancelled', 'Cancelled', 420, 5, null),
|
||||
order(16, 1, 10.5, 'Delivered', 'Delivered', 1350, 15, 1),
|
||||
];
|
||||
|
||||
function bill(
|
||||
id: number,
|
||||
locationid: number,
|
||||
terminalid: string,
|
||||
cashier: string,
|
||||
hours: number,
|
||||
total: number,
|
||||
items: number,
|
||||
mode: string,
|
||||
customer?: string,
|
||||
): PosSale {
|
||||
return {
|
||||
posorderid: 88000 + id,
|
||||
terminalorderid: `b7f${String(id).padStart(4, '0')}-4a2c-11f0-9cd2-0242ac120002`,
|
||||
invoicenumber: `${terminalid}/${String(1200 + id)}`,
|
||||
tenantid: 1135,
|
||||
locationid,
|
||||
terminalid,
|
||||
cashiername: cashier,
|
||||
customerid: customer ? 4000 + id : 0,
|
||||
customername: customer ?? '',
|
||||
billedat: hoursAgo(hours),
|
||||
businessdate: new Date().toISOString().slice(0, 10),
|
||||
subtotal: Math.round(total * 0.91),
|
||||
discount: Math.round(total * 0.02),
|
||||
taxamount: Math.round(total * 0.09),
|
||||
roundoff: 0.4,
|
||||
total,
|
||||
itemcount: items,
|
||||
paymentmode: mode,
|
||||
batchid: `batch-${locationid}-${Math.ceil(hours)}`,
|
||||
receivedat: hoursAgo(hours - 0.01),
|
||||
};
|
||||
}
|
||||
|
||||
export const DEMO_POS_BILLS: Record<number, PosSale[]> = {
|
||||
1: [
|
||||
bill(1, 1, 'RSP-01', 'Anitha', 0.2, 486, 6, 'UPI', 'Meena Ravi'),
|
||||
bill(2, 1, 'RSP-01', 'Anitha', 0.9, 1240, 14, 'Cash'),
|
||||
bill(3, 1, 'RSP-01', 'Anitha', 1.8, 312, 3, 'UPI'),
|
||||
bill(4, 1, 'RSP-01', 'Ramesh', 3.4, 875, 9, 'Card', 'Arun Kumar'),
|
||||
bill(5, 1, 'RSP-01', 'Ramesh', 5.1, 204, 2, 'UPI'),
|
||||
],
|
||||
2: [
|
||||
bill(6, 2, 'PLM-01', 'Kavitha', 0.1, 1580, 18, 'UPI', 'Karthik S'),
|
||||
bill(7, 2, 'PLM-01', 'Kavitha', 0.6, 640, 7, 'Cash'),
|
||||
bill(8, 2, 'PLM-02', 'Selvam', 1.2, 2340, 26, 'Card', 'Lakshmi N'),
|
||||
bill(9, 2, 'PLM-01', 'Kavitha', 2.5, 415, 4, 'UPI'),
|
||||
bill(10, 2, 'PLM-02', 'Selvam', 4.0, 980, 11, 'Cash'),
|
||||
bill(11, 2, 'PLM-01', 'Kavitha', 6.3, 1120, 13, 'UPI', 'Divya Prakash'),
|
||||
],
|
||||
3: [
|
||||
bill(12, 3, 'SRV-01', 'Bhuvana', 3.3, 720, 8, 'Cash', 'Divya Prakash'),
|
||||
bill(13, 3, 'SRV-01', 'Bhuvana', 4.8, 395, 4, 'UPI'),
|
||||
bill(14, 3, 'SRV-01', 'Bhuvana', 7.5, 1260, 15, 'Cash'),
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Delivery jobs — a SEPARATE fixture from the orders above, because they are a
|
||||
* separate read with a different shape.
|
||||
*
|
||||
* Two things are deliberately imperfect. `actualkms` exceeds `kms` on a couple
|
||||
* of jobs, because that gap is the reason the table shows both. And the delivery
|
||||
* status uses its own ladder (accepted / picked / active), not the order one —
|
||||
* a demo that reused the order words would make the two maps look redundant.
|
||||
*/
|
||||
export const DEMO_DELIVERIES: DeliveryRow[] = [
|
||||
{
|
||||
deliveryid: 5001, orderheaderid: 7001, orderid: 'RM24001', tenantid: 1135, locationid: 2,
|
||||
deliverydate: hoursAgo(0.7),
|
||||
locationname: 'R mart — Peelamedu', orderstatus: 'active',
|
||||
assigntime: hoursAgo(0.6), starttime: hoursAgo(0.55), pickuptime: hoursAgo(0.4),
|
||||
itemcount: 9, orderamount: 862, deliveryamt: 862, deliverycharges: 45,
|
||||
pickupcustomer: 'R mart — Peelamedu', pickupsuburb: 'Peelamedu', pickupcontactno: '9879879877',
|
||||
deliverycustomer: 'Karthik S', deliverycontactno: '9840044556',
|
||||
deliveryaddress: '7, Nehru Nagar, Peelamedu', deliverysuburb: 'Peelamedu',
|
||||
ridername: 'Suresh M', ridercontact: '9000011122', kms: '3.0', actualkms: '3.4',
|
||||
transitminutes: 18, ordernotes: 'Call on arrival, gate code 4412',
|
||||
},
|
||||
{
|
||||
deliveryid: 5002, orderheaderid: 7002, orderid: 'RM24002', tenantid: 1135, locationid: 2,
|
||||
deliverydate: hoursAgo(1.4),
|
||||
locationname: 'R mart — Peelamedu', orderstatus: 'delivered',
|
||||
assigntime: hoursAgo(1.3), pickuptime: hoursAgo(1.1), deliverytime: hoursAgo(0.6),
|
||||
itemcount: 14, orderamount: 1240, deliveryamt: 1240, deliverycharges: 55,
|
||||
pickupcustomer: 'R mart — Peelamedu', pickupsuburb: 'Peelamedu',
|
||||
deliverycustomer: 'Divya Prakash', deliverycontactno: '9840077889',
|
||||
deliveryaddress: '22, Sathy Road, Saravanampatti', deliverysuburb: 'Saravanampatti',
|
||||
ridername: 'Vignesh R', ridercontact: '9000011133', kms: '4.0', actualkms: '9.2',
|
||||
transitminutes: 41, notes: 'Wrong pin dropped, rider re-routed',
|
||||
},
|
||||
{
|
||||
deliveryid: 5003, orderheaderid: 7005, orderid: 'RM24005', tenantid: 1135, locationid: 2,
|
||||
deliverydate: hoursAgo(4.9),
|
||||
locationname: 'R mart — Peelamedu', orderstatus: 'delivered',
|
||||
assigntime: hoursAgo(4.8), pickuptime: hoursAgo(4.6), deliverytime: hoursAgo(3.9),
|
||||
itemcount: 22, orderamount: 2180, deliveryamt: 2180, deliverycharges: 60,
|
||||
pickupcustomer: 'R mart — Peelamedu', pickupsuburb: 'Peelamedu',
|
||||
deliverycustomer: 'Meena Ravi', deliverycontactno: '9840012233',
|
||||
deliveryaddress: '12, Bharathi Street, RS Puram', deliverysuburb: 'RS Puram',
|
||||
ridername: 'Prakash T', ridercontact: '9000011144', kms: '2.0', actualkms: '2.1',
|
||||
transitminutes: 14,
|
||||
},
|
||||
{
|
||||
deliveryid: 5004, orderheaderid: 7004, orderid: 'RM24004', tenantid: 1135, locationid: 2,
|
||||
deliverydate: hoursAgo(3.1),
|
||||
locationname: 'R mart — Peelamedu', orderstatus: 'pending',
|
||||
itemcount: 7, orderamount: 690, deliveryamt: 690, deliverycharges: 0,
|
||||
pickupcustomer: 'R mart — Peelamedu', pickupsuburb: 'Peelamedu',
|
||||
deliverycustomer: 'Lakshmi N', deliverycontactno: '9840033445',
|
||||
deliveryaddress: '3, Avinashi Road, Peelamedu', deliverysuburb: 'Peelamedu',
|
||||
kms: '1.8', ordernotes: 'Waiting on packing',
|
||||
},
|
||||
{
|
||||
deliveryid: 5005, orderheaderid: 7009, orderid: 'RM24009', tenantid: 1135, locationid: 3,
|
||||
deliverydate: hoursAgo(2.3),
|
||||
locationname: 'R mart — Saravanampatti', orderstatus: 'picked',
|
||||
assigntime: hoursAgo(2.2), pickuptime: hoursAgo(1.9),
|
||||
itemcount: 12, orderamount: 1105, deliveryamt: 1105, deliverycharges: 50,
|
||||
pickupcustomer: 'R mart — Saravanampatti', pickupsuburb: 'Saravanampatti',
|
||||
deliverycustomer: 'Lakshmi N', deliverycontactno: '9840033445',
|
||||
deliveryaddress: '3, Avinashi Road, Peelamedu', deliverysuburb: 'Peelamedu',
|
||||
ridername: 'Prakash T', ridercontact: '9000011144', kms: '6.0', actualkms: '6.3',
|
||||
transitminutes: 27,
|
||||
},
|
||||
{
|
||||
deliveryid: 5006, orderheaderid: 7008, orderid: 'RM24008', tenantid: 1135, locationid: 3,
|
||||
deliverydate: hoursAgo(1.0),
|
||||
locationname: 'R mart — Saravanampatti', orderstatus: 'accepted',
|
||||
assigntime: hoursAgo(0.9),
|
||||
itemcount: 6, orderamount: 540, deliveryamt: 540, deliverycharges: 40,
|
||||
pickupcustomer: 'R mart — Saravanampatti', pickupsuburb: 'Saravanampatti',
|
||||
deliverycustomer: 'Arun Kumar', deliverycontactno: '9840099001',
|
||||
deliveryaddress: '90, Cross Cut Road, Gandhipuram', deliverysuburb: 'Gandhipuram',
|
||||
ridername: 'Suresh M', ridercontact: '9000011122', kms: '5.5',
|
||||
},
|
||||
{
|
||||
deliveryid: 5007, orderheaderid: 7010, orderid: 'RM24010', tenantid: 1135, locationid: 3,
|
||||
deliverydate: hoursAgo(5.7),
|
||||
locationname: 'R mart — Saravanampatti', orderstatus: 'delivered',
|
||||
assigntime: hoursAgo(5.6), pickuptime: hoursAgo(5.4), deliverytime: hoursAgo(4.8),
|
||||
itemcount: 8, orderamount: 780, deliveryamt: 780, deliverycharges: 45,
|
||||
pickupcustomer: 'R mart — Saravanampatti', pickupsuburb: 'Saravanampatti',
|
||||
deliverycustomer: 'Meena Ravi', deliverycontactno: '9840012233',
|
||||
deliveryaddress: '12, Bharathi Street, RS Puram', deliverysuburb: 'RS Puram',
|
||||
ridername: 'Suresh M', ridercontact: '9000011122', kms: '2.0', actualkms: '2.4',
|
||||
transitminutes: 16,
|
||||
},
|
||||
{
|
||||
deliveryid: 5008, orderheaderid: 7006, orderid: 'RM24006', tenantid: 1135, locationid: 2,
|
||||
deliverydate: hoursAgo(6.2),
|
||||
locationname: 'R mart — Peelamedu', orderstatus: 'cancelled',
|
||||
canceltime: hoursAgo(6.0),
|
||||
itemcount: 4, orderamount: 320, deliveryamt: 0, deliverycharges: 0,
|
||||
pickupcustomer: 'R mart — Peelamedu', pickupsuburb: 'Peelamedu',
|
||||
deliverycustomer: 'Karthik S', deliverycontactno: '9840044556',
|
||||
deliveryaddress: '7, Nehru Nagar, Peelamedu', deliverysuburb: 'Peelamedu',
|
||||
kms: '3.0', notes: 'Customer cancelled before pickup',
|
||||
},
|
||||
{
|
||||
deliveryid: 5009, orderheaderid: 7012, orderid: 'RM24012', tenantid: 1135, locationid: 1,
|
||||
deliverydate: hoursAgo(1.7),
|
||||
locationname: 'R mart — RS Puram', orderstatus: 'skipped',
|
||||
assigntime: hoursAgo(1.6),
|
||||
itemcount: 2, orderamount: 180, deliveryamt: 180, deliverycharges: 35,
|
||||
pickupcustomer: 'R mart — RS Puram', pickupsuburb: 'RS Puram',
|
||||
deliverycustomer: 'Divya Prakash', deliverycontactno: '9840077889',
|
||||
deliveryaddress: '22, Sathy Road, Saravanampatti', deliverysuburb: 'Saravanampatti',
|
||||
ridername: 'Vignesh R', kms: '7.2', notes: 'Nobody at address, returned to store',
|
||||
},
|
||||
];
|
||||
|
||||
/* ────────────────────────────────────────────────────────────────────────────
|
||||
People — back office and till accounts
|
||||
|
||||
Two separate sets on purpose, because they are two separate account systems.
|
||||
Note the till accounts carry NO password or pin: the backend returns those
|
||||
only in the answer to a creation, never in a listing, and a fixture that
|
||||
handed them back in a list would teach the UI a habit the real API refuses.
|
||||
──────────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
import type { PosRole, PosUser, StaffInfo, StaffShift } from '@/api/types';
|
||||
|
||||
export const DEMO_STAFF: StaffInfo[] = [
|
||||
{
|
||||
userid: 9002, rolename: 'Administrator', roleid: 1, fullname: 'Rmart Operations',
|
||||
firstname: 'Rmart', lastname: 'Operations', email: 'rmart@gmail.com', contactno: '9876543210',
|
||||
tenantid: 1135, locationid: 1, locationname: 'R mart — RS Puram', status: 'Active',
|
||||
},
|
||||
{
|
||||
userid: 9010, rolename: 'Manager', roleid: 4, fullname: 'Anitha Selvam',
|
||||
firstname: 'Anitha', lastname: 'Selvam', email: 'anitha@rmart.com', contactno: '9840111222',
|
||||
tenantid: 1135, locationid: 1, locationname: 'R mart — RS Puram', status: 'Active',
|
||||
},
|
||||
{
|
||||
userid: 9011, rolename: 'Manager', roleid: 4, fullname: 'Vijay Anand',
|
||||
firstname: 'Vijay', lastname: 'Anand', email: 'vijay@rmart.com', contactno: '9840111333',
|
||||
tenantid: 1135, locationid: 2, locationname: 'R mart — Peelamedu', status: 'Active',
|
||||
},
|
||||
{
|
||||
userid: 9012, rolename: 'Staff', roleid: 5, fullname: 'Priya Raman',
|
||||
firstname: 'Priya', lastname: 'Raman', email: 'priya@rmart.com', contactno: '9840111444',
|
||||
tenantid: 1135, locationid: 2, locationname: 'R mart — Peelamedu', status: 'Active',
|
||||
},
|
||||
{
|
||||
userid: 9013, rolename: 'Staff', roleid: 5, fullname: 'Mohan Das',
|
||||
firstname: 'Mohan', lastname: 'Das', email: 'mohan@rmart.com', contactno: '9840111555',
|
||||
tenantid: 1135, locationid: 3, locationname: 'R mart — Saravanampatti', status: 'Inactive',
|
||||
},
|
||||
];
|
||||
|
||||
export const DEMO_POS_ROLES: PosRole[] = [
|
||||
{
|
||||
role_id: 7, role: 'supervisor', label: 'Supervisor',
|
||||
description:
|
||||
'Runs the terminal: imports, settings, voids, and creating counter staff. ' +
|
||||
'Signs in at a till only — a till account has no Nearle Daily login.',
|
||||
},
|
||||
{
|
||||
role_id: 8, role: 'cashier', label: 'Cashier',
|
||||
description: 'Billing only.',
|
||||
},
|
||||
];
|
||||
|
||||
export const DEMO_SHIFTS: Record<number, StaffShift[]> = {
|
||||
1: [
|
||||
{ staff_shift_id: 11, tenantid: 1135, locationid: 1, name: 'Morning', start_time: '08:00', end_time: '15:00', weekdays: '1111100', status: 'Active' },
|
||||
{ staff_shift_id: 12, tenantid: 1135, locationid: 1, name: 'Evening', start_time: '15:00', end_time: '22:00', weekdays: '1111111', status: 'Active' },
|
||||
],
|
||||
2: [
|
||||
{ staff_shift_id: 13, tenantid: 1135, locationid: 2, name: 'Morning', start_time: '07:00', end_time: '15:00', weekdays: '1111111', status: 'Active' },
|
||||
{ staff_shift_id: 14, tenantid: 1135, locationid: 2, name: 'Late', start_time: '15:00', end_time: '23:00', weekdays: '1111111', status: 'Active' },
|
||||
],
|
||||
3: [
|
||||
{ staff_shift_id: 15, tenantid: 1135, locationid: 3, name: 'Full day', start_time: '08:00', end_time: '22:00', weekdays: '', status: 'Active' },
|
||||
],
|
||||
};
|
||||
|
||||
export const DEMO_POS_USERS: Record<number, PosUser[]> = {
|
||||
1: [
|
||||
{ user_id: 7001, full_name: 'Anitha Selvam', role: 'supervisor', role_id: 7, contactno: '9840111222', has_password: true, shift_id: 11, shift_name: 'Morning', shift_start: '08:00', shift_end: '15:00', location_id: 1, status: 'Active' },
|
||||
{ user_id: 7002, full_name: 'Ramesh K', role: 'cashier', role_id: 8, contactno: '9840222111', has_password: true, shift_id: 12, shift_name: 'Evening', shift_start: '15:00', shift_end: '22:00', location_id: 1, status: 'Active' },
|
||||
],
|
||||
2: [
|
||||
{ user_id: 7003, full_name: 'Kavitha M', role: 'supervisor', role_id: 7, contactno: '9840333111', has_password: true, shift_id: 13, shift_name: 'Morning', shift_start: '07:00', shift_end: '15:00', location_id: 2, status: 'Active' },
|
||||
{ user_id: 7004, full_name: 'Selvam R', role: 'cashier', role_id: 8, contactno: '9840333222', has_password: true, shift_id: 14, shift_name: 'Late', shift_start: '15:00', shift_end: '23:00', location_id: 2, status: 'Active' },
|
||||
{ user_id: 7005, full_name: 'Deepa N', role: 'cashier', role_id: 8, contactno: '9840333444', has_password: false, location_id: 2, status: 'Inactive' },
|
||||
],
|
||||
3: [
|
||||
{ user_id: 7006, full_name: 'Bhuvana S', role: 'supervisor', role_id: 7, contactno: '9840444111', has_password: true, shift_id: 15, shift_name: 'Full day', shift_start: '08:00', shift_end: '22:00', location_id: 3, status: 'Active' },
|
||||
],
|
||||
};
|
||||
217
src/demo/index.ts
Normal file
217
src/demo/index.ts
Normal file
@@ -0,0 +1,217 @@
|
||||
/**
|
||||
* Demo mode — three signed-in roles and a fixture backend, for working on the
|
||||
* console without reaching Fiesta.
|
||||
*
|
||||
* WHY THIS EXISTS. The API is wired and correct, but it has never been
|
||||
* exercised end to end from a development machine that cannot reach
|
||||
* `fiesta.nearle.app`, and the Store Admin and Store Manager workspaces have no
|
||||
* accounts to sign into yet. Rather than leave every screen showing an error,
|
||||
* demo mode seeds a session and answers the same endpoints from fixtures.
|
||||
*
|
||||
* HOW IT IS GATED — two locks, and both must be open:
|
||||
*
|
||||
* 1. `import.meta.env.DEV` — a build-time constant. In a production build
|
||||
* this is `false`, so every branch below is dead code that the bundler
|
||||
* removes entirely. The fixtures never ship.
|
||||
* 2. A `sessionStorage` flag the person sets by clicking a demo button. Even
|
||||
* in development, a normal sign-in against the real API is unaffected
|
||||
* unless demo mode was explicitly chosen.
|
||||
*
|
||||
* This is a development affordance, NOT an authentication bypass: it cannot be
|
||||
* reached in a production build at all, and it grants nothing on the server —
|
||||
* it only decides what this browser tab renders.
|
||||
*/
|
||||
|
||||
const FLAG = 'nearle.demo.v1';
|
||||
|
||||
/** Compile-time gate. `false` in a production build, so everything below drops. */
|
||||
export const DEMO_AVAILABLE = import.meta.env.DEV;
|
||||
|
||||
export function isDemoActive(): boolean {
|
||||
if (!DEMO_AVAILABLE) return false;
|
||||
try {
|
||||
return sessionStorage.getItem(FLAG) === 'on';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function enableDemo(): void {
|
||||
sessionStorage.setItem(FLAG, 'on');
|
||||
}
|
||||
|
||||
export function disableDemo(): void {
|
||||
sessionStorage.removeItem(FLAG);
|
||||
}
|
||||
|
||||
/* ────────────────────────────────────────────────────────────────────────────
|
||||
The fixture backend
|
||||
──────────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* Answers a request from fixtures, or returns `MISS` to let the real call go
|
||||
* out.
|
||||
*
|
||||
* `MISS` is a sentinel rather than `undefined` because an endpoint legitimately
|
||||
* answering `undefined` and an endpoint we have no fixture for are different
|
||||
* things, and collapsing them would silently swallow the second.
|
||||
*/
|
||||
export const MISS = Symbol('demo-miss');
|
||||
|
||||
/**
|
||||
* Answers a request from fixtures, or returns `MISS` to let the real call go
|
||||
* out.
|
||||
*
|
||||
* `MISS` is a sentinel rather than `undefined` because an endpoint legitimately
|
||||
* answering `undefined` and an endpoint we have no fixture for are different
|
||||
* things, and collapsing them would silently swallow the second.
|
||||
*
|
||||
* The fixtures are behind a dynamic import INSIDE the `import.meta.env.DEV`
|
||||
* guard, and that is load-bearing rather than stylistic: with a static import
|
||||
* the bundler cannot prove the module is unreachable, so the whole fixture set
|
||||
* ships to production. Verified by grepping the built assets for fixture
|
||||
* strings — see the note in the README.
|
||||
*/
|
||||
export async function demoResolve(
|
||||
path: string,
|
||||
params: Record<string, unknown> | undefined,
|
||||
): Promise<unknown | typeof MISS> {
|
||||
if (!import.meta.env.DEV) return MISS;
|
||||
|
||||
const f = await import('./fixtures');
|
||||
const tenantid = Number(params?.['tenantid'] ?? 0);
|
||||
|
||||
if (path.endsWith('/tenants/getalltenants')) return f.DEMO_TENANTS;
|
||||
|
||||
if (path.endsWith('/tenants/gettenantlocations')) {
|
||||
return f.DEMO_LOCATIONS[tenantid] ?? [];
|
||||
}
|
||||
|
||||
if (path.endsWith('/catalogue/getproducts')) {
|
||||
const brand = String(params?.['brand'] ?? '');
|
||||
const keyword = String(params?.['keyword'] ?? '').toLowerCase();
|
||||
return f.DEMO_CATALOGUE.filter(
|
||||
(product) =>
|
||||
(brand === '' || product.brand === brand) &&
|
||||
(keyword === '' || product.product_name.toLowerCase().includes(keyword)),
|
||||
);
|
||||
}
|
||||
|
||||
if (path.endsWith('/catalogue/getbrands')) return f.DEMO_BRANDS;
|
||||
|
||||
if (path.endsWith('/products/getimportedcatalogueproducts')) {
|
||||
return f.DEMO_IMPORTED[tenantid] ?? [];
|
||||
}
|
||||
|
||||
if (path.endsWith('/products/getproductcategories')) return f.DEMO_CATEGORIES;
|
||||
|
||||
if (path.endsWith('/products/getlocationproducts')) return f.DEMO_STORE_PRODUCTS;
|
||||
|
||||
if (path.endsWith('/orders/getlocationsummary')) {
|
||||
return f.DEMO_LOCATION_SUMMARY[tenantid] ?? [];
|
||||
}
|
||||
|
||||
if (path.endsWith('/orders/getordersummary')) {
|
||||
return f.DEMO_ORDER_SUMMARY[tenantid] ?? {};
|
||||
}
|
||||
|
||||
/* ── Store Admin ──────────────────────────────────────────────────────── */
|
||||
|
||||
if (path.endsWith('/orders/tenant/getorders') || path.endsWith('/orders/getorders')) {
|
||||
const locationid = Number(params?.['locationid'] ?? 0);
|
||||
const status = String(params?.['status'] ?? '');
|
||||
const keyword = String(params?.['keyword'] ?? '').toLowerCase();
|
||||
return f.DEMO_ORDERS.filter(
|
||||
(order) =>
|
||||
(!locationid || order.locationid === locationid) &&
|
||||
(status === '' || order.orderstatus === status) &&
|
||||
(keyword === '' ||
|
||||
`${order.orderid} ${order.deliverycustomer}`.toLowerCase().includes(keyword)),
|
||||
);
|
||||
}
|
||||
|
||||
if (path.endsWith('/deliveries/getdeliveries')) {
|
||||
const locationid = Number(params?.['locationid'] ?? 0);
|
||||
const keyword = String(params?.['keyword'] ?? '').toLowerCase();
|
||||
return f.DEMO_DELIVERIES.filter(
|
||||
(job) =>
|
||||
(!locationid || job.locationid === locationid) &&
|
||||
(keyword === '' ||
|
||||
`${job.orderid} ${job.deliverycustomer} ${job.ridername}`
|
||||
.toLowerCase()
|
||||
.includes(keyword)),
|
||||
);
|
||||
}
|
||||
|
||||
if (path.includes('/pos/sales') && !path.includes('summary') && !path.includes('detail')) {
|
||||
const locationid = Number(params?.['locationid'] ?? 0);
|
||||
const bills = f.DEMO_POS_BILLS[locationid] ?? [];
|
||||
return { total: bills.length, pageno: 0, pagesize: 100, bills };
|
||||
}
|
||||
|
||||
if (path.includes('/health/location')) {
|
||||
const locationid = Number(params?.['location_id'] ?? 0);
|
||||
return f.DEMO_POS_HEALTH[locationid] ?? [];
|
||||
}
|
||||
|
||||
if (path.includes('/sales/summary')) {
|
||||
const locationid = Number(params?.['locationid'] ?? 0);
|
||||
return f.DEMO_POS_SUMMARY[locationid] ?? {};
|
||||
}
|
||||
|
||||
if (path.endsWith('/tenants/getstaffs')) return f.DEMO_STAFF;
|
||||
|
||||
if (path.endsWith('/posroles')) return f.DEMO_POS_ROLES;
|
||||
|
||||
if (path.endsWith('/getposusers')) {
|
||||
const locationid = Number(params?.['locationid'] ?? 0);
|
||||
return f.DEMO_POS_USERS[locationid] ?? [];
|
||||
}
|
||||
|
||||
if (path.endsWith('/getstaffshifts')) {
|
||||
const locationid = Number(params?.['locationid'] ?? 0);
|
||||
return f.DEMO_SHIFTS[locationid] ?? [];
|
||||
}
|
||||
|
||||
// A created till account answers with its PIN once — the same shape the real
|
||||
// endpoint uses, so the "shown once" screen is exercised in demo too.
|
||||
if (path.includes('/createposuser')) {
|
||||
return { user_id: 7099, full_name: 'New till user', role: 'cashier', status: 'Active', pin: '4821' };
|
||||
}
|
||||
if (path.includes('/updateposuser')) return { user_id: 7099, status: 'Active' };
|
||||
if (path.includes('/users/create')) return { userid: 9099, status: 'Active' };
|
||||
if (path.includes('/users/update')) return { userid: 9099, status: 'Active' };
|
||||
|
||||
if (path.endsWith('/products/getstockrequests')) {
|
||||
const locationid = Number(params?.['locationid'] ?? 0);
|
||||
const status = String(params?.['status'] ?? '');
|
||||
return f.DEMO_STOCK_REQUESTS.filter(
|
||||
(request) =>
|
||||
(!locationid || request.locationid === locationid) &&
|
||||
(status === '' || request.status === status),
|
||||
);
|
||||
}
|
||||
|
||||
if (path.endsWith('/products/getstockstatement')) {
|
||||
const locationid = Number(params?.['locationid'] ?? 0);
|
||||
const keyword = String(params?.['keyword'] ?? '').toLowerCase();
|
||||
const rows = f.DEMO_STOCK_STATEMENT[locationid] ?? [];
|
||||
return keyword === ''
|
||||
? rows
|
||||
: rows.filter((row) => (row.productname ?? '').toLowerCase().includes(keyword));
|
||||
}
|
||||
|
||||
if (path.includes('/updatestockrequest')) return { updated: true };
|
||||
|
||||
// Writes acknowledge without persisting: the point of demo mode is to walk
|
||||
// the screens, and a form that silently fails teaches the wrong thing.
|
||||
if (path.includes('/createtenantlocation')) {
|
||||
return { tenantid: 1140, locationid: 20, tenantname: 'New tenant', status: 'Active' };
|
||||
}
|
||||
if (path.includes('/createlocation')) {
|
||||
return { locationid: 21, tenantid, locationname: 'New branch', status: 'Active' };
|
||||
}
|
||||
if (path.includes('/importcatalogueproduct')) return { imported: true };
|
||||
|
||||
return MISS;
|
||||
}
|
||||
Reference in New Issue
Block a user