Updated project changes
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* Shared payment store — the single source of truth that lets the OWNER/ADMIN
|
||||
* side and the CUSTOMER side talk to each other in this demo (no backend).
|
||||
*
|
||||
* Both invoices and transactions live in localStorage so a payment request the
|
||||
* owner creates appears in the customer's Outstanding list, and a payment the
|
||||
* customer makes (or the owner marks manually) shows back on the owner's
|
||||
* Payment Management page. In production these reads/writes would hit the CRM
|
||||
* billing API instead — the component surface would stay the same.
|
||||
*/
|
||||
|
||||
import { INVOICES } from './invoices';
|
||||
|
||||
const INVOICES_KEY = 'lynkeduppro.payments.invoices';
|
||||
const TXNS_KEY = 'lynkeduppro.payments.transactions';
|
||||
const UPDATE_EVENT = 'payments:updated';
|
||||
|
||||
/**
|
||||
* Default customer mobile number for the demo. The owner/admin "New Payment
|
||||
* Request" form prefills this so a request lands with the current demo
|
||||
* customer. In production this comes from a customer lookup instead.
|
||||
*/
|
||||
export const DEMO_CUSTOMER_PHONE = '214-555-2600';
|
||||
|
||||
/** Compare two phone numbers by digits only (ignores spaces, dashes, etc.). */
|
||||
export const normalizePhone = (phone) => String(phone || '').replace(/\D/g, '');
|
||||
|
||||
/**
|
||||
* Does this invoice belong to the given customer phone? Invoices without a
|
||||
* `customerPhone` are treated as unassigned and visible to any customer (keeps
|
||||
* the original seed invoices working).
|
||||
*/
|
||||
export const invoiceMatchesCustomer = (invoice, phone) =>
|
||||
!invoice.customerPhone ||
|
||||
normalizePhone(invoice.customerPhone) === normalizePhone(phone);
|
||||
|
||||
const read = (key, fallback) => {
|
||||
try {
|
||||
const raw = localStorage.getItem(key);
|
||||
return raw ? JSON.parse(raw) : fallback;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
};
|
||||
|
||||
const write = (key, value) => {
|
||||
try {
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
} catch {
|
||||
/* localStorage unavailable (private mode) — best effort only. */
|
||||
}
|
||||
};
|
||||
|
||||
/** Notify same-tab listeners (the native `storage` event only fires cross-tab). */
|
||||
const emit = () => {
|
||||
try {
|
||||
window.dispatchEvent(new Event(UPDATE_EVENT));
|
||||
} catch {
|
||||
/* non-browser / SSR — nothing to notify. */
|
||||
}
|
||||
};
|
||||
|
||||
/** Subscribe to any invoice/transaction change. Returns an unsubscribe fn. */
|
||||
export const subscribe = (callback) => {
|
||||
window.addEventListener(UPDATE_EVENT, callback);
|
||||
window.addEventListener('storage', callback);
|
||||
return () => {
|
||||
window.removeEventListener(UPDATE_EVENT, callback);
|
||||
window.removeEventListener('storage', callback);
|
||||
};
|
||||
};
|
||||
|
||||
/* ------------------------------- Invoices -------------------------------- */
|
||||
|
||||
/** Load invoices, seeding from the static sample list on first run. */
|
||||
export const loadInvoices = () => {
|
||||
const stored = read(INVOICES_KEY, null);
|
||||
if (Array.isArray(stored)) return stored;
|
||||
write(INVOICES_KEY, INVOICES);
|
||||
return INVOICES;
|
||||
};
|
||||
|
||||
export const saveInvoices = (list) => {
|
||||
write(INVOICES_KEY, list);
|
||||
emit();
|
||||
};
|
||||
|
||||
export const getInvoice = (id) => loadInvoices().find((i) => i.id === id) || null;
|
||||
|
||||
/** Append a new invoice (payment request) and return it. */
|
||||
export const addInvoice = (invoice) => {
|
||||
const next = [...loadInvoices(), invoice];
|
||||
saveInvoices(next);
|
||||
return invoice;
|
||||
};
|
||||
|
||||
/** Next sequential invoice id, e.g. "INV-2068". */
|
||||
export const nextInvoiceId = () => {
|
||||
const max = loadInvoices().reduce((m, inv) => {
|
||||
const n = parseInt(String(inv.id).replace(/\D/g, ''), 10);
|
||||
return Number.isFinite(n) && n > m ? n : m;
|
||||
}, 2000);
|
||||
return `INV-${max + 1}`;
|
||||
};
|
||||
|
||||
/* ----------------------------- Transactions ------------------------------ */
|
||||
|
||||
export const loadTransactions = () => read(TXNS_KEY, []);
|
||||
|
||||
export const saveTransactions = (list) => {
|
||||
write(TXNS_KEY, list);
|
||||
emit();
|
||||
};
|
||||
|
||||
/** Has this invoice been paid (by the customer or marked by the owner)? */
|
||||
export const isInvoicePaid = (invoiceId) =>
|
||||
loadTransactions().some((t) => t.invoiceId === invoiceId && t.status === 'paid');
|
||||
|
||||
/**
|
||||
* Mark an invoice paid by writing a `paid` transaction — used both by the
|
||||
* customer success flow and by the owner's manual "Mark as Paid" action.
|
||||
* No-ops if a paid transaction for this invoice already exists.
|
||||
*/
|
||||
export const markInvoicePaid = (invoiceId, { method = 'manual' } = {}) => {
|
||||
const inv = getInvoice(invoiceId);
|
||||
if (!inv || isInvoicePaid(invoiceId)) return null;
|
||||
|
||||
const txn = {
|
||||
id: `${method}-${invoiceId}-${Date.now()}`,
|
||||
invoiceId,
|
||||
amount: inv.amount,
|
||||
currency: inv.currency,
|
||||
customerEmail: null,
|
||||
status: 'paid',
|
||||
createdAt: new Date().toISOString(),
|
||||
method,
|
||||
};
|
||||
saveTransactions([txn, ...loadTransactions()]);
|
||||
return txn;
|
||||
};
|
||||
Reference in New Issue
Block a user