/* global React, AppShell, foyerApi, Icon, FoyerLoader */
// ============================================================
// Offers — CRUD library of marketing offers / campaigns.
//
// Mirrors the iPad Offers tab exactly: a list of cards with name +
// body preview, an enabled toggle per offer, "New offer" pill at the
// top, and an inline editor sheet for creating / editing.
//
// Offers participate in the AI follow-up pipeline two ways:
// 1. Free-form @mention in any prompt ("send the @Spring Buyer
// credit blast to all buyers") — the AI loads the offer body and
// uses it as context.
// 2. Default library access — even WITHOUT an explicit @mention, the
// AI may pull from the enabled offer pool when picking the best
// angle for a lead. Disabled offers are excluded.
//
// Backend: /me/offers (GET/POST), /me/offers/{id} (PATCH/DELETE),
// /me/offers/{id}/enabled (POST). Same wire as the iPad app — both
// surfaces see the same library because it's stored server-side.
// ============================================================
function OffersPage() {
const [offers, setOffers] = React.useState([]);
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState(null);
// editing is one of:
// null — no sheet open
// {} — sheet open for a new offer
// { id, … } — sheet open for editing an existing offer
const [editing, setEditing] = React.useState(null);
const [pendingDelete, setPendingDelete] = React.useState(null);
const refresh = React.useCallback(async () => {
setLoading(true);
try {
const r = await foyerApi.get('/me/offers');
setOffers(r.offers || []);
setError(null);
} catch (e) {
setError(e.message || String(e));
} finally {
setLoading(false);
}
}, []);
React.useEffect(() => { refresh(); }, [refresh]);
const toggleEnabled = async (offer) => {
// Optimistic flip so the toggle feels instant. Revert on error.
const next = !offer.enabled;
setOffers(curr => curr.map(o => o.id === offer.id ? { ...o, enabled: next } : o));
try {
await foyerApi.post(`/me/offers/${offer.id}/enabled`, { enabled: next });
} catch (e) {
setOffers(curr => curr.map(o => o.id === offer.id ? { ...o, enabled: !next } : o));
window.foyerToast({ message: 'Could not save toggle: ' + (e.message || e), kind: 'error' });
}
};
const onSaved = (offer) => {
setOffers(curr => {
const idx = curr.findIndex(o => o.id === offer.id);
if (idx === -1) return [...curr, offer];
const next = [...curr];
next[idx] = offer;
return next;
});
setEditing(null);
window.foyerToast('Offer saved');
};
const performDelete = async (offer) => {
setPendingDelete(null);
try {
await foyerApi.del(`/me/offers/${offer.id}`);
setOffers(curr => curr.filter(o => o.id !== offer.id));
window.foyerToast('Offer deleted');
} catch (e) {
window.foyerToast({ message: 'Delete failed: ' + (e.message || e), kind: 'error' });
}
};
return (
Marketing offers and campaigns the AI can reference. Mention
one in any prompt with @name, or leave it
enabled and the AI will pull from the library on its own.
Offers
Create your first campaign — Open House Copilot can drop it into follow-ups when the lead is a fit, or you can call it out by name in a prompt.
{offer.name} will be removed from your library. The AI won't suggest it anymore. This can't be undone.