Updated project changes

This commit is contained in:
2026-06-12 20:49:34 +05:30
parent aa87870350
commit f45955191f
13 changed files with 566 additions and 55 deletions
+140
View File
@@ -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;
};