import { useState, useRef } from "react";
const C = {
deepBg:"#112e30", tealDeep:"#1a6e72", tealMid:"#229FA4",
tealBright:"#64C9C4", tealLight:"#85D8D4", tealPale:"#B2E8E6",
tealWash:"#E8F8F8", offWhite:"#F5FAFA", warmGray:"#F0F6F6",
textDark:"#112628", textMid:"#2a5254", textMuted:"#5c8486",
borderLight:"rgba(34,159,164,0.13)", borderMid:"rgba(34,159,164,0.25)",
gold:"#c4a832", amber:"#854F0B", amberBg:"#FAEEDA",
green:"#3B6D11", greenBg:"#EAF3DE", red:"#A32D2D", redBg:"#FCEBEB",
};
const EJS = { serviceId:"YOUR_SERVICE_ID", teamTemplateId:"YOUR_TEAM_TEMPLATE_ID", clientTemplateId:"YOUR_CLIENT_TEMPLATE_ID", publicKey:"YOUR_PUBLIC_KEY", twwuEmail:"hello@theworldwithinustravels.com" };
const DEMO = EJS.publicKey === "YOUR_PUBLIC_KEY";
async function sendEmail(tid, params) {
if (DEMO) return { demo:true };
const res = await fetch("https://api.emailjs.com/api/v1.0/email/send", { method:"POST", headers:{"Content-Type":"application/json"}, body:JSON.stringify({ service_id:EJS.serviceId, template_id:tid, user_id:EJS.publicKey, template_params:params }) });
if (!res.ok) throw new Error(await res.text());
return { sent:true };
}
async function notifyTeam({ eventType, proposal, clientName, clientEmail, detail, actionBy }) {
const ts = new Date().toLocaleString("en-US",{weekday:"short",month:"short",day:"numeric",year:"numeric",hour:"numeric",minute:"2-digit"});
return sendEmail(EJS.teamTemplateId, { to_email:EJS.twwuEmail, event_type:eventType, proposal, client:clientName, client_email:clientEmail||"not provided", detail, action_by:actionBy, timestamp:ts });
}
async function notifyClient({ clientEmail, clientName, destination, dates, eventType, detail, curatorName }) {
if (!clientEmail) return { skipped:true };
return sendEmail(EJS.clientTemplateId, { to_email:clientEmail, client_name:clientName, destination, dates, event_type:eventType, detail, curator_name:curatorName||"Shammy", twwu_email:EJS.twwuEmail });
}
const TEAM = [
{ id:"anina", name:"Anina Monteforte", role:"CEO", dept:"Leadership", initials:"AM", color:"#7a5520", bg:"#f5e9c8", pipelineAccess:true, financeAccess:true },
{ id:"shammy", name:"Shammy", role:"Chief of Staff", dept:"Chief of Staff Office", initials:"SH", color:C.tealDeep, bg:C.tealWash, pipelineAccess:true, financeAccess:true },
{ id:"gpLead1", name:"G&P Lead", role:"Growth & Partnerships Lead", dept:"Growth & Partnerships", initials:"GL", color:"#596b15", bg:"#eaf0c4", pipelineAccess:true, financeAccess:false },
{ id:"gpLead2", name:"G&P Manager", role:"Growth & Partnerships Manager", dept:"Growth & Partnerships", initials:"GM", color:"#596b15", bg:"#eaf0c4", pipelineAccess:true, financeAccess:false },
{ id:"tolg", name:"Travel Ops Lead (Group)", role:"Travel Ops Lead — Group Trips", dept:"Travel Operations", initials:"TG", color:"#3B6D11", bg:"#EAF3DE", pipelineAccess:true, financeAccess:false },
{ id:"tolc", name:"Oliwia Warczok", role:"Travel Ops Lead — Custom Trips", dept:"Travel Operations", initials:"OW", color:"#3B6D11", bg:"#EAF3DE", pipelineAccess:true, financeAccess:false },
{ id:"bsLead", name:"Brand Lead", role:"Brand & Storytelling Lead", dept:"Brand & Storytelling", initials:"BL", color:"#3a6878", bg:"#d8edf2", pipelineAccess:true, financeAccess:false },
{ id:"celead1", name:"CE Lead 1", role:"CE & Success Lead", dept:"CE & Success", initials:"CL", color:"#3a6878", bg:"#d8edf2", pipelineAccess:false, financeAccess:false },
{ id:"celead2", name:"CE Lead 2", role:"CE & Success Lead", dept:"CE & Success", initials:"CL", color:"#3a6878", bg:"#d8edf2", pipelineAccess:false, financeAccess:false },
{ id:"pm1", name:"Project Manager 1",role:"PM", dept:"CE & Success", initials:"PM", color:"#596b15", bg:"#eaf0c4", pipelineAccess:false, financeAccess:false },
{ id:"psolead1",name:"PSO Lead 1", role:"PSO Lead", dept:"Proposal Strategy & Ops",initials:"PL",color:"#229FA4", bg:"#E8F8F8", pipelineAccess:false, financeAccess:false },
{ id:"psolead2",name:"PSO Lead 2", role:"PSO Lead", dept:"Proposal Strategy & Ops",initials:"PL",color:"#229FA4", bg:"#E8F8F8", pipelineAccess:false, financeAccess:false },
{ id:"psopm1", name:"PSO PM 1", role:"PSO PM", dept:"Proposal Strategy & Ops",initials:"PP",color:"#1a6e72", bg:"#B2E8E6", pipelineAccess:false, financeAccess:false },
];
const TRIP_TYPES = ["Group Trips","Retreats","Custom Trips","Corporate Retreats"];
const PIPELINE_STAGES_GROUP = [
{ id:"lead", label:"Lead Received", color:"#B4B2A9" },
{ id:"outreach", label:"Outreach Sent", color:"#888780" },
{ id:"callsched",label:"Call Scheduled", color:C.tealMid },
{ id:"callcomp", label:"Call Completed", color:C.tealDeep },
{ id:"booked", label:"Deposit Paid / Booked",color:C.green },
{ id:"noproc", label:"Not Proceeding", color:C.red },
];
const PIPELINE_STAGES_CUSTOM = [
{ id:"lead", label:"Lead Received", color:"#B4B2A9" },
{ id:"emailsent",label:"Initial Email Sent", color:"#888780" },
{ id:"formcomp", label:"Inquiry Form Complete", color:C.tealMid },
{ id:"callsched",label:"Call Scheduled w/ Anina",color:C.tealMid },
{ id:"callcomp", label:"Call Completed", color:C.tealDeep },
{ id:"feepaid", label:"Design Fee Paid", color:C.green },
{ id:"proposal", label:"→ Proposal Phase", color:"#7a5520" },
{ id:"noproc", label:"Not Proceeding", color:C.red },
];
const LEAD_STATUSES = [
{ id:"inquired", label:"Inquired", color:"#888780" },
{ id:"outreach", label:"Outreach Sent", color:C.tealMid },
{ id:"pending", label:"Deposit Pending", color:C.amber },
{ id:"booked", label:"Booked", color:C.green },
{ id:"noproc", label:"Not Proceeding", color:C.red },
];
const STATUS_LABELS = { draft:"Draft", review:"In Review", feedback:"Feedback Received", approved:"Approved", handed:"Handed to Operations" };
const STATUS_COLORS = {
draft: { bg:"#F1EFE8", color:"#5F5E5A", bar:"#B4B2A9" },
review: { bg:C.tealWash, color:C.tealMid, bar:C.tealLight },
feedback: { bg:C.amberBg, color:C.amber, bar:"#EF9F27" },
approved: { bg:C.greenBg, color:C.green, bar:"#639922" },
handed: { bg:"#d8edf2", color:"#3a6878", bar:"#3a6878" },
};
const PKG = {
luxe: { label:"✦ Luxe", bg:"#f5e9c8", color:"#7a5520", border:"#e8d5a8", sel:"#a0732c" },
gold: { label:"◈ Gold", bg:"#eaf0c4", color:"#596b15", border:"#dde5b0", sel:"#8a9a2a" },
silver: { label:"◇ Silver", bg:"#d8edf2", color:"#3a6878", border:"#b8d5dc", sel:"#4e8090" },
};
const INIT_LEADS_GROUP = [
{ id:"lg1", name:"Maria Santos", contact:"maria@email.com", source:"Instagram", tripInterest:"Morocco Group Escape Apr 2026", status:"callcomp", assignedTo:"celead1", notes:"Called Mar 10, very interested.", dateReceived:"Mar 8, 2026", lastContact:"Mar 10, 2026" },
{ id:"lg2", name:"Keisha Brown", contact:"keisha@email.com", source:"Referral — Text", tripInterest:"El Salvador Retreat Sept 2026", status:"callsched",assignedTo:"celead2", notes:"Referred by past client.", dateReceived:"Apr 10, 2026", lastContact:"Apr 11, 2026" },
{ id:"lg3", name:"Priya Nair", contact:"priya@email.com", source:"Website Inquiry Form",tripInterest:"Morocco Group Escape Apr 2026",status:"booked", assignedTo:"celead1", notes:"Deposit paid via WeTravel.", dateReceived:"Mar 1, 2026", lastContact:"Mar 20, 2026" },
{ id:"lg4", name:"Janet Flores", contact:"", source:"Instagram", tripInterest:"Group Retreat TBD", status:"lead", assignedTo:"", notes:"", dateReceived:"Apr 12, 2026", lastContact:"" },
];
const INIT_LEADS_CUSTOM = [
{ id:"lc1", name:"Dennis Heyman", contact:"dennis@example.com", source:"Referral — Email", tripType:"Group Birthday Trip", travelDates:"Jul 2026", groupSize:"10", services:["Luxury Accommodation","Curated Experiences","Private Guide or Driver"], status:"feepaid", assignedTo:"psolead1", notes:"Morocco birthday trip. Design fee $650 paid.", dateReceived:"Mar 5, 2026", lastContact:"Mar 18, 2026", designPackage:"Wellness Retreat / Group — $650", ninetyDay:false },
{ id:"lc2", name:"Kwame Asante", contact:"kwame@email.com", source:"Social Media", tripType:"Group Birthday Trip", travelDates:"Aug 2026", groupSize:"8", services:["Luxury Accommodation","Flights & Transfers","Curated Experiences"], status:"callcomp", assignedTo:"celead1", notes:"Ghana birthday trip. Discussing design fee.", dateReceived:"Mar 28, 2026", lastContact:"Apr 5, 2026", designPackage:"", ninetyDay:false },
{ id:"lc3", name:"Amanda & James", contact:"amanda@example.com", source:"Referral — Text", tripType:"Honeymoon", travelDates:"May 2026", groupSize:"2", services:["Luxury Accommodation","Dining Reservations","24/7 Concierge Service"], status:"proposal", assignedTo:"psopm1", notes:"Proposal approved. Handed to ops.", dateReceived:"Feb 14, 2026", lastContact:"Mar 30, 2026", designPackage:"Week Getaway — $425", ninetyDay:true },
{ id:"lc4", name:"Self-Care for Latinas",contact:"retreat@example.com", source:"Website Inquiry Form",tripType:"Retreat", travelDates:"Nov 2026", groupSize:"20", services:["Luxury Accommodation","Curated Experiences","Flights & Transfers"], status:"lead", assignedTo:"", notes:"El Salvador annual retreat inquiry.", dateReceived:"Apr 11, 2026", lastContact:"", designPackage:"", ninetyDay:false },
];
const INIT_SPONSORED_TRIPS = [
{ id:"st1", name:"Morocco Group Escape", dates:"Apr 22–28, 2026", destination:"Marrakech, Morocco", totalSeats:16, bookedSeats:11, depositPending:2, sources:{social:18,wetravel:7}, booked:{social:8,wetravel:3},
leads:[
{ id:"wl1", name:"Maria Santos", email:"maria@email.com", source:"wetravel", status:"booked", dateAdded:"Mar 8, 2026", notes:"Paid deposit via WeTravel booking page" },
{ id:"wl2", name:"Keisha Brown", email:"keisha@email.com", source:"social", status:"booked", dateAdded:"Mar 10, 2026", notes:"DM via Instagram" },
{ id:"wl3", name:"Priya Nair", email:"priya@email.com", source:"wetravel", status:"booked", dateAdded:"Mar 12, 2026", notes:"WeTravel booking page" },
{ id:"wl4", name:"Janet Flores", email:"", source:"social", status:"inquired", dateAdded:"Apr 12, 2026", notes:"Story reply on Instagram" },
{ id:"wl5", name:"Diane Okafor", email:"diane@email.com", source:"wetravel", status:"pending", dateAdded:"Apr 8, 2026", notes:"Deposit pending confirmation" },
]},
{ id:"st2", name:"El Salvador Retreat", dates:"Sept 12–18, 2026", destination:"El Salvador", totalSeats:20, bookedSeats:6, depositPending:4, sources:{social:22,wetravel:4}, booked:{social:5,wetravel:1},
leads:[
{ id:"wl6", name:"Carmen Rivera", email:"carmen@email.com", source:"social", status:"booked", dateAdded:"Mar 20, 2026", notes:"Instagram reel inquiry" },
{ id:"wl7", name:"Tanya Williams",email:"tanya@email.com", source:"wetravel", status:"booked", dateAdded:"Mar 22, 2026", notes:"WeTravel page booking" },
{ id:"wl8", name:"Lucia Mendez", email:"lucia@email.com", source:"social", status:"pending", dateAdded:"Apr 5, 2026", notes:"Facebook DM, deposit pending" },
]},
{ id:"st3", name:"Ghana Cultural Journey",dates:"Aug 5–14, 2026", destination:"Accra & Cape Coast, Ghana", totalSeats:14, bookedSeats:3, depositPending:1, sources:{social:9,wetravel:2}, booked:{social:3,wetravel:0},
leads:[
{ id:"wl9", name:"Abena Asante",email:"abena@email.com", source:"social", status:"booked", dateAdded:"Apr 1, 2026", notes:"TikTok comment DM booked" },
{ id:"wl10", name:"Grace Mensah", email:"grace@email.com", source:"wetravel", status:"inquired", dateAdded:"Apr 9, 2026", notes:"WeTravel inquiry, not yet booked" },
]},
];
const INIT_PROPOSALS = [
{ id:"marrakech", destination:"Marrakech, Morocco", client:"Dennis Heyman", clientEmail:"dennis@example.com", tripType:"Group Trips", groupSize:10, dates:"Jul 10–17, 2026", nights:6, status:"review", activity:"Shared 2 days ago", comments:3, assignedTo:"psolead1", conversations:[
{ name:"Dennis Heyman", role:"client", text:"The riad looks stunning! Is the rooftop available every evening?", time:"Mar 18, 4:20pm" },
{ name:"Shammy", role:"team", text:"The rooftop is yours every evening!", time:"Mar 18, 5:10pm" },
]},
{ id:"peru", destination:"Sacred Valley, Peru", client:"Self-Care for Latinas", clientEmail:"retreat@example.com", tripType:"Retreats", groupSize:20, dates:"Nov 2026", nights:7, status:"feedback", activity:"Feedback 1 day ago", comments:5, assignedTo:"psopm1", conversations:[
{ name:"Self-Care for Latinas", role:"client", text:"Can we swap Day 3 for the salt flats?", time:"Apr 10, 2:00pm" },
]},
{ id:"hawaii", destination:"Maui, Hawaii", client:"Amanda & James", clientEmail:"amanda@example.com", tripType:"Custom Trips", groupSize:2, dates:"May 2026", nights:10, status:"approved", activity:"Approved Mar 14", comments:8, assignedTo:"psolead2", conversations:[] },
{ id:"elsalvador", destination:"El Salvador", client:"Self-Care Retreat", clientEmail:"", tripType:"Retreats", groupSize:15, dates:"Sept 2026", nights:5, status:"draft", activity:"Last edited today", comments:0, assignedTo:"psopm1", conversations:[] },
];
const HOTELS = {
luxe:[
{ id:"l1", name:"Dar Sukkar", stars:5, price:"$1,200", desc:"Private riad exclusively for your group. Central Medina, rooftop terrace, plunge pool.", amenities:["Entire riad exclusively reserved","Private rooftop + plunge pool","Dedicated house staff 24/7","Daily breakfast included"] },
{ id:"l2", name:"La Sultana Marrakech", stars:5, price:"$1,450", desc:"Five interconnected riads in the Kasbah. Rooftop pool, award-winning spa.", amenities:["Rooftop pool with Medina views","Full-service spa access","Butler service","Curated daily programs"] },
{ id:"l3", name:"Royal Mansour", stars:5, price:"$2,200", desc:"The most iconic address in Marrakech — private riads with royal-level service.", amenities:["Private riad with personal staff","Royal Mansour Spa","Three signature restaurants","Bespoke experience concierge"] },
],
gold:[
{ id:"g1", name:"Riad Kniza", stars:4, price:"$720", desc:"Antique-filled boutique riad, family-run with exceptional personal service.", amenities:["Courtyard pool and lounge","Rooftop terrace with views","Breakfast and afternoon tea","In-house hammam"] },
{ id:"g2", name:"Riad Fes Maya", stars:4, price:"$640", desc:"Elegant 18th-century residence with Andalusian gardens and a rooftop terrace.", amenities:["Andalusian courtyard garden","Rooftop dining terrace","Daily breakfast included","Concierge experience desk"] },
{ id:"g3", name:"Les Jardins de la Koutoubia", stars:4, price:"$580", desc:"Contemporary hotel near Jemaa el-Fna with two pools and a lush garden.", amenities:["Two outdoor pools","Heated indoor pool + spa","Central Medina location","Welcome dinner included"] },
],
silver:[
{ id:"s1", name:"Riad Yasmine", stars:3, price:"$320", desc:"Colorful, photogenic riad with a signature pool and warm hospitality.", amenities:["Signature turquoise pool","Rooftop lounge area","Breakfast included"] },
{ id:"s2", name:"Dar Darma", stars:3, price:"$280", desc:"Boutique riad with artisan-crafted interiors in the heart of the souk.", amenities:["Courtyard lounge","Artisan-tiled interiors","Breakfast included"] },
{ id:"s3", name:"Maison MK", stars:3, price:"$260", desc:"Minimal, design-forward riad with a serene courtyard.", amenities:["Design-led interiors","Courtyard pool","Daily breakfast included"] },
],
};
const ADDONS = [
{ id:"a1", icon:"🎈", name:"Private Hot Air Balloon", desc:"Sunrise balloon over the Palmeraie with champagne landing", price:"$320/person" },
{ id:"a2", icon:"🍲", name:"Private Cooking Class", desc:"Moroccan cuisine with a master chef in a traditional kitchen", price:"$120/person" },
{ id:"a3", icon:"🐪", name:"Desert Day Trip — Agafay", desc:"Camel trek, luxury desert camp lunch, sunset views", price:"$195/person" },
{ id:"a4", icon:"🛍️", name:"Personal Shopping Guide", desc:"Half-day guide to the finest artisans and boutiques", price:"$85/person" },
{ id:"a5", icon:"✍️", name:"Calligraphy Workshop", desc:"Private Arabic calligraphy with a local master artist", price:"$75/person" },
{ id:"a6", icon:"📸", name:"Professional Photography", desc:"2-hour Medina session with edited gallery in 5 days", price:"$250/group" },
{ id:"a7", icon:"✨", name:"Couples Hammam Ritual", desc:"Private hammam and argan oil treatment at the riad", price:"$160/couple" },
{ id:"a8", icon:"🌙", name:"Airport VIP Meet & Greet", desc:"Fast-track and private lounge access on arrival", price:"$95/person" },
];
const DAYS_DEFAULT = [
{ title:"Arrival & Welcome Dinner — Nouba Rooftop", desc:"Private airport transfers to Dar Sukkar riad. Welcome refreshments. Evening: private rooftop dinner catered by Nouba.", meals:"Dinner included", hotel:"Dar Sukkar Riad" },
{ title:"Medina at Dawn + Hammam & Wellness", desc:"Private guided sunrise walk through the Medina. Afternoon: group hammam and spa treatments at La Sultana Spa.", meals:"Breakfast included", hotel:"Dar Sukkar Riad" },
{ title:"Ourika Valley Mountain Excursion", desc:"Full-day private excursion to the Ourika Valley. Berber village visit, waterfall hike, and mountain lodge lunch.", meals:"Breakfast + Lunch included", hotel:"Dar Sukkar Riad" },
{ title:"Artisan Workshop + Safran by Koya Dinner", desc:"Morning at the leather tanneries with private artisan workshop. Evening: private dining at Safran by Koya.", meals:"Breakfast + Dinner included",hotel:"Dar Sukkar Riad" },
{ title:"Free Day + Theatro Farewell Evening", desc:"Morning at leisure. Afternoon poolside. Evening: farewell dinner at Theatro.", meals:"Breakfast + Dinner included",hotel:"Dar Sukkar Riad" },
{ title:"Departure Day", desc:"Late checkout. Final breakfast at the riad. Private transfers to Marrakech Menara Airport.", meals:"Breakfast included", hotel:"—" },
];
const PROPOSAL_STAGES = [
{ id:"s1", num:"01", title:"Client Brief Handoff", dept:"CE & Success", status:"Handoff Complete", statusColor:"#3B6D11", statusBg:"#EAF3DE", owners:["CE Lead","PM"],
checklist:["Full profile shared with PSO","ClickUp status set to Proposal Phase","Design fee confirmed"],
fields:[
{ key:"clientProfile", label:"Client Profile Summary", type:"textarea", placeholder:"Summarize client profile, trip type, preferences, constraints..." },
{ key:"designPackage", label:"Design Package", type:"select", options:["Mini Escape — $250","Week Getaway — $425","Adventure of a Lifetime — $550","Wellness Retreat / Group — $650","TBD"] },
{ key:"designFee", label:"Design Fee Confirmed", type:"select", options:["Yes","No","Pending"] },
{ key:"clickupStatus", label:"ClickUp Status Set", type:"select", options:["Yes — Proposal Phase","Not yet"] },
{ key:"briefNotes", label:"Additional Brief Notes", type:"textarea", placeholder:"Specific client requests, constraints, or context from CE..." },
]},
{ id:"s2", num:"02", title:"Proposal Development", dept:"PSO", status:"In Development", statusColor:C.tealMid, statusBg:C.tealWash, owners:["PSO Lead","PSO PM"],
checklist:["ClickUp project set up","Destinations researched","Draft proposal built"],
fields:[
{ key:"clickupProject", label:"ClickUp Project Created", type:"select", options:["Yes","No"] },
{ key:"destinations", label:"Destinations Researched", type:"textarea", placeholder:"List destinations considered, rationale for selected option..." },
{ key:"draftStatus", label:"Draft Proposal Status", type:"select", options:["Not started","In progress","Draft complete"] },
{ key:"draftNotes", label:"Draft Development Notes", type:"textarea", placeholder:"Research notes, key elements of the proposal..." },
{ key:"deliveryTarget", label:"Proposal Delivery Target Date", type:"text", placeholder:"e.g. April 10, 2026" },
]},
{ id:"s3", num:"03", title:"Vendor Scoping", dept:"PSO", status:"Vendor Scoping", statusColor:"#7a5520", statusBg:"#f5e9c8", owners:["PSO Lead","PSO PM"],
checklist:["Partners identified","Availability & quotes confirmed","Logged in ClickUp"],
fields:[
{ key:"vendors", label:"Vendors / Partners Identified", type:"textarea", placeholder:"Hotels, experiences, transfers, dining, guides..." },
{ key:"availability", label:"Availability Confirmed", type:"select", options:["All confirmed","Partially confirmed","Pending","Not started"] },
{ key:"quotes", label:"Quotes Obtained", type:"select", options:["All received","Partially received","Pending"] },
{ key:"clickupLogged", label:"Logged in ClickUp", type:"select", options:["Yes","No","In progress"] },
{ key:"vendorNotes", label:"Vendor Notes & Holds", type:"textarea", placeholder:"Holds, deadlines, contingencies, backup options..." },
]},
{ id:"s4", num:"04", title:"Budget Alignment", dept:"PSO", status:"Budget Review", statusColor:"#854F0B", statusBg:"#FAEEDA", owners:["PSO Lead","PSO PM"],
checklist:["Full cost model built","Aligned to design package","Scope changes flagged to CE"],
fields:[
{ key:"costModel", label:"Cost Model Status", type:"select", options:["Complete","In progress","Not started"] },
{ key:"totalBudget", label:"Total Trip Cost (USD)", type:"text", placeholder:"e.g. $24,000" },
{ key:"perPersonCost", label:"Per Person Cost (USD)", type:"text", placeholder:"e.g. $3,000" },
{ key:"packageAlignment", label:"Aligned to Design Package", type:"select", options:["Yes — fully aligned","Partially aligned — see notes","No — CE review needed"] },
{ key:"scopeChanges", label:"Scope Changes Flagged to CE", type:"select", options:["No changes","Yes — flagged","Pending CE review"] },
{ key:"budgetNotes", label:"Budget Notes", type:"textarea", placeholder:"Cost breakdown, scope change details, CE feedback..." },
]},
{ id:"s5", num:"05", title:"Proposal Production", dept:"PSO + CE", status:"Proposal Ready", statusColor:C.tealMid, statusBg:C.tealWash, owners:["PSO Team","CE Lead"],
checklist:["Built in WeTravel","Canva deck prepared (backup)","CE Lead review complete"],
fields:[
{ key:"wetravelStatus", label:"Built in WeTravel", type:"select", options:["Complete","In progress","Not started"] },
{ key:"canvaDeck", label:"Canva Deck Prepared", type:"select", options:["Complete","In progress","Not needed","Not started"] },
{ key:"ceReview", label:"CE Lead Review", type:"select", options:["Approved","In review","Not started","Revisions requested"] },
{ key:"proposalLink", label:"WeTravel / Proposal Link", type:"text", placeholder:"Paste proposal URL here..." },
{ key:"productionNotes",label:"Production Notes", type:"textarea", placeholder:"Final proposal notes, design decisions, CE feedback..." },
]},
{ id:"s6", num:"06", title:"Client Presentation", dept:"CE & Success", status:"Presented", statusColor:"#3a6878", statusBg:"#d8edf2", owners:["Anina","CE & Success Lead"],
checklist:["CE Lead presents proposal","Client questions addressed","Feedback logged in ClickUp"],
fields:[
{ key:"presentationDate", label:"Presentation Date", type:"text", placeholder:"e.g. April 15, 2026" },
{ key:"presentationMethod", label:"Presentation Method", type:"select", options:["Video call","In-person","Email / async","Portal share"] },
{ key:"clientQuestions", label:"Client Questions / Feedback", type:"textarea", placeholder:"Log all client questions and responses..." },
{ key:"feedbackLogged", label:"Feedback Logged in ClickUp", type:"select", options:["Yes","No","In progress"] },
{ key:"presentationNotes", label:"Presentation Notes", type:"textarea", placeholder:"Overall impressions, client reaction, follow-up items..." },
]},
{ id:"s7", num:"07", title:"Client Approval", dept:"CE & Success", status:"Approved / Revise", statusColor:"#112e30", statusBg:"#B2E8E6", owners:["PSO Lead","PSO PM","CE & Success Lead"],
checklist:["Approval formally collected","OR: revisions fed back to PSO","ClickUp status updated"],
fields:[
{ key:"approvalStatus", label:"Approval Status", type:"select", options:["Approved","Revisions requested","Pending","Declined"] },
{ key:"approvalDate", label:"Approval Date", type:"text", placeholder:"e.g. April 18, 2026" },
{ key:"revisionDetails", label:"Revision Details (if applicable)", type:"textarea", placeholder:"Detail any revisions requested — fed back to PSO..." },
{ key:"clickupFinal", label:"ClickUp Status Updated", type:"select", options:["Yes — Approved","Yes — Revise","Not yet"] },
{ key:"approvalNotes", label:"Approval Notes", type:"textarea", placeholder:"Final notes, conditions of approval, next steps..." },
]},
];
function exportProposalJSON(proposal, stageData, days, selectedHotel, selectedAddons) {
const hotel = Object.values(HOTELS).flat().find(h=>h.id===selectedHotel)||null;
const addons = ADDONS.filter(a=>selectedAddons.includes(a.id));
const payload = {
_twwu_schema:"ops-handoff-v1",
exportedAt:new Date().toISOString(),
exportedBy:"Sales & Proposal Portal",
proposal:{ id:proposal.id, destination:proposal.destination, client:proposal.client, clientEmail:proposal.clientEmail, tripType:proposal.tripType, groupSize:proposal.groupSize, dates:proposal.dates, nights:proposal.nights, status:"Approved" },
itinerary:days,
accommodation:hotel,
addOns:addons,
proposalStages:stageData,
handoff:{ triggeredAt:new Date().toISOString(), status:"Ready for Operations", notes:"Auto-generated from approved proposal." },
};
const blob = new Blob([JSON.stringify(payload,null,2)],{type:"application/json"});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href=url; a.download=`TWWU_OpsHandoff_${proposal.destination.replace(/[^a-zA-Z0-9]/g,"_")}_${new Date().toISOString().slice(0,10)}.json`; a.click();
URL.revokeObjectURL(url);
}
function parseWeCSV(text) {
const lines = text.trim().split("\n").filter(l=>l.trim());
if (lines.length<2) return [];
const headers = lines[0].split(",").map(h=>h.trim().toLowerCase().replace(/[^a-z0-9]/g,""));
const nameIdx = headers.findIndex(h=>h.includes("name")||h.includes("participant"));
const emailIdx = headers.findIndex(h=>h.includes("email"));
const dateIdx = headers.findIndex(h=>h.includes("date")||h.includes("created"));
const statusIdx= headers.findIndex(h=>h.includes("status")||h.includes("payment"));
return lines.slice(1).map((line,i)=>{
const cols = line.split(",").map(c=>c.trim().replace(/^"|"$/g,""));
const rawStatus = statusIdx>=0 ? cols[statusIdx]?.toLowerCase() : "";
const status = rawStatus.includes("paid")||rawStatus.includes("complet")||rawStatus.includes("confirm") ? "booked"
: rawStatus.includes("pend") ? "pending" : "inquired";
return {
id:"csv_"+Date.now()+"_"+i,
name:nameIdx>=0?cols[nameIdx]||"Unknown":`Participant ${i+1}`,
email:emailIdx>=0?cols[emailIdx]||"":"",
source:"wetravel", status,
dateAdded:dateIdx>=0?cols[dateIdx]||new Date().toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"}):new Date().toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"}),
notes:"Imported from WeTravel CSV",
};
}).filter(r=>r.name&&r.name!=="Unknown"||r.email);
}
const panel = {background:"white",border:`1px solid ${C.borderLight}`,borderRadius:12,overflow:"hidden",marginBottom:20};
const ph = {padding:"14px 20px",borderBottom:`1px solid ${C.borderLight}`,display:"flex",alignItems:"center",justifyContent:"space-between"};
const pt = {fontSize:11,fontWeight:500,letterSpacing:"0.08em",textTransform:"uppercase",color:C.textMuted};
const pb = {padding:20};
const inputSt = {width:"100%",border:`1px solid ${C.borderLight}`,borderRadius:7,padding:"9px 12px",fontFamily:"inherit",fontSize:13,color:C.textDark,outline:"none",background:"white"};
const labelSt = {fontSize:11,letterSpacing:"0.1em",textTransform:"uppercase",color:C.textMuted,marginBottom:5,display:"block",fontWeight:500};
function btn(v,extra={}) {
return {fontFamily:"inherit",fontSize:12,fontWeight:500,padding:"7px 14px",borderRadius:6,cursor:"pointer",border:"none",display:"inline-flex",alignItems:"center",gap:6,transition:"all 0.15s",...extra,
...(v==="primary"?{background:C.tealMid,color:"white"}:v==="outline"?{background:"transparent",color:C.tealMid,border:`1px solid ${C.tealLight}`}:v==="danger"?{background:C.redBg,color:C.red,border:`1px solid rgba(163,45,45,0.2)`}:{background:"transparent",color:C.textMid,border:`1px solid ${C.borderMid}`})};
}
function Badge({status}) {
const sc = STATUS_COLORS[status]||STATUS_COLORS.draft;
return
{STATUS_LABELS[status]||status} ;
}
function Stars({n}) { return
{"★".repeat(n)}{"☆".repeat(5-n)} ; }
function Toast({msg}) {
return msg ?
{msg}
: null;
}
function Avatar({member,size=28}) {
if (!member) return null;
return
{member.initials}
;
}
function PipelineStatusPill({label,color}) {
return
{label} ;
}
// ─── LOGIN ────────────────────────────────────────────────────────────────────
function LoginScreen({onLogin}) {
const [search,setSearch] = useState("");
const [selected,setSelected] = useState(null);
const depts = [...new Set(TEAM.map(t=>t.dept))];
const filtered = TEAM.filter(t=>t.name.toLowerCase().includes(search.toLowerCase())||t.role.toLowerCase().includes(search.toLowerCase())||t.dept.toLowerCase().includes(search.toLowerCase()));
return (
The World Within Us
Sales & Proposal Portal
Internal Team Access
Select your name to continue
setSearch(e.target.value)} placeholder="Search by name or role..." style={{width:"100%",background:"rgba(255,255,255,0.06)",border:"1px solid rgba(255,255,255,0.1)",borderRadius:8,padding:"10px 14px",fontFamily:"inherit",fontSize:13,color:"white",outline:"none",marginBottom:16}}/>
{depts.map(dept=>{
const members=filtered.filter(t=>t.dept===dept);
if(!members.length) return null;
return (
{dept}
{members.map(member=>(
setSelected(member.id)} style={{display:"flex",alignItems:"center",gap:12,padding:"10px 12px",borderRadius:8,cursor:"pointer",border:`1px solid ${selected===member.id?"rgba(100,201,196,0.4)":"transparent"}`,background:selected===member.id?"rgba(100,201,196,0.1)":"transparent",marginBottom:3}}>
{member.name}
{member.role}
{member.financeAccess && Finance Access }
{!member.financeAccess && member.pipelineAccess && Pipeline Access }
{selected===member.id && ✓
}
))}
);
})}
{if(selected) onLogin(TEAM.find(t=>t.id===selected));}} disabled={!selected} style={{width:"100%",marginTop:20,padding:"12px",borderRadius:8,border:"none",background:selected?C.tealMid:"rgba(255,255,255,0.08)",color:selected?"white":"rgba(255,255,255,0.3)",fontFamily:"inherit",fontSize:14,fontWeight:500,cursor:selected?"pointer":"not-allowed"}}>
{selected?`Continue as ${TEAM.find(t=>t.id===selected)?.name.split(" ")[0]} →`:"Select your name to continue"}
onLogin({id:"client_demo",name:"Dennis Heyman",role:"Client",initials:"DH",color:C.amber,bg:C.amberBg,isClient:true,pipelineAccess:false,financeAccess:false})} style={{fontSize:11,color:"rgba(178,232,230,0.35)",background:"none",border:"none",cursor:"pointer",textDecoration:"underline",fontFamily:"inherit"}}>Preview as client →
);
}
// ─── FINANCE DASHBOARD ────────────────────────────────────────────────────────
const DEMO_FINANCE = {
summary:{ totalRevenue:284500, totalCosts:162800, netProfit:121700, margin:42.8, confirmedRevenue:198000, pipelineValue:86500 },
byTripType:[
{ type:"Group Trips", revenue:112000, costs:67200, profit:44800, margin:40, trips:3 },
{ type:"Retreats", revenue:78500, costs:44100, profit:34400, margin:43.8, trips:2 },
{ type:"Custom Trips", revenue:64000, costs:36200, profit:27800, margin:43.4, trips:4 },
{ type:"Corporate Retreats", revenue:30000, costs:15300, profit:14700, margin:49.0, trips:1 },
],
byTrip:[
{ name:"Morocco Group Escape", type:"Group Trips", revenue:48000, costs:28800, profit:19200, status:"confirmed", client:"Dennis Heyman" },
{ name:"El Salvador Retreat", type:"Retreats", revenue:38500, costs:22000, profit:16500, status:"confirmed", client:"Self-Care for Latinas" },
{ name:"Maui Honeymoon", type:"Custom Trips", revenue:22000, costs:12400, profit:9600, status:"approved", client:"Amanda & James" },
{ name:"Ghana Cultural Journey", type:"Group Trips", revenue:32000, costs:19000, profit:13000, status:"pipeline", client:"Kwame Asante" },
{ name:"Sacred Valley Retreat", type:"Retreats", revenue:40000, costs:22100, profit:17900, status:"confirmed", client:"Self-Care for Latinas" },
{ name:"Corporate Retreat TBD", type:"Corporate Retreats", revenue:30000, costs:15300, profit:14700, status:"pipeline", client:"TBD" },
],
designFees:{ collected:4250, outstanding:1300, total:5550 },
monthly:[
{ month:"Jan", revenue:18000, costs:10800 },
{ month:"Feb", revenue:24000, costs:14200 },
{ month:"Mar", revenue:42000, costs:24000 },
{ month:"Apr", revenue:38500, costs:22100 },
{ month:"May", revenue:51000, costs:29200 },
{ month:"Jun", revenue:44000, costs:25100 },
],
};
function FinanceDashboard({currentUser,toast}) {
const [uploadedData,setUploadedData] = useState(null);
const [showUpload,setShowUpload] = useState(false);
const fileRef = useRef(null);
const data = uploadedData || DEMO_FINANCE;
const isDemo = !uploadedData;
const handleFile = (e) => {
const file = e.target.files[0]; if(!file) return;
const reader = new FileReader();
reader.onload = (ev) => {
try {
const parsed = JSON.parse(ev.target.result);
setUploadedData(parsed); setShowUpload(false);
toast("Finance data loaded successfully!");
} catch(err) { toast("Could not parse JSON file. Please check the format."); }
};
reader.readAsText(file);
};
if (!currentUser.financeAccess) {
return (
🔒
Private Dashboard
This dashboard is restricted to the CEO and Chief of Staff.
);
}
const fmt = (n) => "$"+Math.round(n).toLocaleString();
const maxRev = Math.max(...data.monthly.map(m=>m.revenue));
const tripStatusColor = {confirmed:{bg:C.greenBg,color:C.green},approved:{bg:C.tealWash,color:C.tealMid},pipeline:{bg:C.amberBg,color:C.amber}};
return (
Private — CEO & Chief of Staff Only
Finance & Sales Dashboard
{isDemo && Demo data — upload JSON to replace }
setShowUpload(true)} style={{padding:"8px 16px",borderRadius:8,border:"1px solid rgba(196,168,50,0.4)",background:"rgba(196,168,50,0.1)",color:"#c4a832",fontFamily:"inherit",fontSize:12,cursor:"pointer"}}>↑ Upload Finance JSON
{[
{label:"Total Revenue", value:fmt(data.summary.totalRevenue), sub:"confirmed + pipeline", color:C.tealDeep},
{label:"Confirmed Revenue", value:fmt(data.summary.confirmedRevenue), sub:"from booked & approved trips", color:C.green},
{label:"Pipeline Value", value:fmt(data.summary.pipelineValue), sub:"projected, not yet confirmed", color:C.amber},
{label:"Total Costs", value:fmt(data.summary.totalCosts), sub:"all trip expenses", color:C.red},
{label:"Net Profit", value:fmt(data.summary.netProfit), sub:"revenue minus costs", color:C.tealMid},
{label:"Profit Margin", value:`${data.summary.margin}%`, sub:"overall across all trips", color:"#7a5520"},
].map(s=>(
{s.label}
{s.value}
{s.sub}
))}
{data.byTripType.map((t,i)=>{
const pct = Math.round((t.revenue/data.summary.totalRevenue)*100);
return (
{t.type}
{t.trips} trip{t.trips!==1?"s":""} · margin {t.margin}%
{fmt(t.revenue)}
profit {fmt(t.profit)}
);
})}
Monthly Revenue vs. Costs
{[{label:"Revenue",color:C.tealMid},{label:"Costs",color:C.red+"88"}].map(l=>(
{data.monthly.map(m=>(
))}
{["Trip","Type","Revenue","Costs","Profit","Margin","Status"].map(h=>(
{h}
))}
{data.byTrip.map((t,i)=>{
const margin = Math.round((t.profit/t.revenue)*100);
const sc = tripStatusColor[t.status]||tripStatusColor.pipeline;
return (
{t.name}
{t.type}
{fmt(t.revenue)}
{fmt(t.costs)}
{fmt(t.profit)}
{margin}%
{t.status}
);
})}
{[
{label:"Collected", value:fmt(data.designFees.collected), color:C.green},
{label:"Outstanding", value:fmt(data.designFees.outstanding), color:C.amber},
{label:"Total Billed", value:fmt(data.designFees.total), color:C.tealDeep},
].map(f=>(
{f.label}
{f.value}
))}
{Math.round((data.designFees.collected/data.designFees.total)*100)}% of design fees collected
{[
{label:"Confirmed Revenue", value:fmt(data.summary.confirmedRevenue), pct:Math.round((data.summary.confirmedRevenue/data.summary.totalRevenue)*100), color:C.green},
{label:"Pipeline (Projected)",value:fmt(data.summary.pipelineValue), pct:Math.round((data.summary.pipelineValue/data.summary.totalRevenue)*100), color:C.amber},
].map(s=>(
{showUpload && (
e.target===e.currentTarget&&setShowUpload(false)} style={{position:"fixed",inset:0,background:"rgba(8,40,30,0.6)",zIndex:600,display:"flex",alignItems:"center",justifyContent:"center"}}>
Upload Finance Data
Upload your finance JSON package. The dashboard will read the data as-is without modifying anything in the file.
fileRef.current&&fileRef.current.click()}>
📂
Click to upload finance JSON
File must be a valid .json package
setShowUpload(false)}>Cancel
)}
);
}
// ─── SPONSORED TRIP LEADS ─────────────────────────────────────────────────────
function AddWeTravelLead({onClose,onAdd,tripName}) {
const [f,setF] = useState({name:"",email:"",source:"wetravel",status:"inquired",notes:""});
const set=(k,v)=>setF(p=>({...p,[k]:v}));
const handleAdd=()=>{
if(!f.name) return;
onAdd({id:"m_"+Date.now(),...f,dateAdded:new Date().toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"})});
};
return (
Add Lead Manually
{tripName}
Notes
Cancel
Add Lead
);
}
function SponsoredTripLeads({trip,onBack,onUpdate,toast}) {
const [leads,setLeads] = useState(trip.leads||[]);
const [filterSource,setFilterSource] = useState("all");
const [filterStatus,setFilterStatus] = useState("all");
const [showAddModal,setShowAddModal] = useState(false);
const [showCSV,setShowCSV] = useState(false);
const [csvText,setCsvText] = useState("");
const [csvPreview,setCsvPreview] = useState([]);
const [selectedLead,setSelectedLead] = useState(null);
const fileRef = useRef(null);
const syncUp=(newLeads)=>{
setLeads(newLeads);
const booked={social:newLeads.filter(l=>l.source==="social"&&l.status==="booked").length,wetravel:newLeads.filter(l=>l.source==="wetravel"&&l.status==="booked").length};
const sources={social:newLeads.filter(l=>l.source==="social").length,wetravel:newLeads.filter(l=>l.source==="wetravel").length};
const bookedSeats=newLeads.filter(l=>l.status==="booked").length;
const depositPending=newLeads.filter(l=>l.status==="pending").length;
onUpdate({...trip,leads:newLeads,booked,sources,bookedSeats,depositPending});
};
const filtered=leads.filter(l=>(filterSource==="all"||l.source===filterSource)&&(filterStatus==="all"||l.status===filterStatus));
const socialLeads=leads.filter(l=>l.source==="social");
const wtLeads=leads.filter(l=>l.source==="wetravel");
const socialConv=socialLeads.length>0?Math.round((socialLeads.filter(l=>l.status==="booked").length/socialLeads.length)*100):0;
const wtConv=wtLeads.length>0?Math.round((wtLeads.filter(l=>l.status==="booked").length/wtLeads.length)*100):0;
const handleCSVFile=(e)=>{
const file=e.target.files[0]; if(!file) return;
const reader=new FileReader();
reader.onload=(ev)=>{const text=ev.target.result;setCsvText(text);setCsvPreview(parseWeCSV(text));};
reader.readAsText(file);
};
const importCSV=()=>{
if(!csvPreview.length) return;
const newLeads=[...leads,...csvPreview.map(l=>({...l,id:"csv_"+Date.now()+"_"+Math.random()}))];
syncUp(newLeads);setCsvPreview([]);setCsvText("");setShowCSV(false);
toast(`${csvPreview.length} leads imported from WeTravel CSV!`);
};
const updateStatus=(id,status)=>{
const newLeads=leads.map(l=>l.id===id?{...l,status}:l);
syncUp(newLeads);setSelectedLead(sl=>sl?{...sl,status}:sl);toast("Lead status updated!");
};
return (
← Back to Sponsored Trips
TWWU Sponsored Trip
{trip.name}
{trip.destination} · {trip.dates}
setShowCSV(true)} style={{padding:"8px 14px",borderRadius:8,border:"1px solid rgba(100,201,196,0.4)",background:"rgba(100,201,196,0.1)",color:C.tealPale,fontFamily:"inherit",fontSize:12,cursor:"pointer"}}>↑ Import WeTravel CSV
setShowAddModal(true)} style={{padding:"8px 14px",borderRadius:8,border:"1px solid rgba(100,201,196,0.4)",background:"rgba(100,201,196,0.1)",color:C.tealPale,fontFamily:"inherit",fontSize:12,cursor:"pointer"}}>+ Add Lead Manually
{[
{label:"Total Leads", value:leads.length},
{label:"Booked", value:leads.filter(l=>l.status==="booked").length},
{label:"Pending Deposit", value:leads.filter(l=>l.status==="pending").length},
{label:"Open Seats", value:trip.totalSeats-leads.filter(l=>l.status==="booked").length-leads.filter(l=>l.status==="pending").length},
].map(s=>(
))}
{[
{label:"📱 Social Media", leads:socialLeads.length,booked:socialLeads.filter(l=>l.status==="booked").length,pending:socialLeads.filter(l=>l.status==="pending").length,conv:socialConv,color:C.tealMid},
{label:"🌐 WeTravel Booking Page",leads:wtLeads.length,booked:wtLeads.filter(l=>l.status==="booked").length,pending:wtLeads.filter(l=>l.status==="pending").length,conv:wtConv,color:"#7a5520"},
].map(src=>(
{src.label}
{[["Leads In",src.leads,C.textDark],["Booked",src.booked,C.green],["Pending",src.pending,C.amber],["Conv.",`${src.conv}%`,src.color]].map(([l,v,col])=>(
))}
))}
Source:
{[{id:"all",label:"All"},{id:"social",label:"📱 Social"},{id:"wetravel",label:"🌐 WeTravel"}].map(f=>(
setFilterSource(f.id)} style={{...btn(filterSource===f.id?"primary":""),fontSize:11}}>{f.label}
))}
Status:
{[{id:"all",label:"All"},...LEAD_STATUSES].map(s=>(
setFilterStatus(s.id)} style={{...btn(filterStatus===s.id?"outline":""),fontSize:11}}>{s.label||"All"}
))}
{filtered.map(lead=>{
const st=LEAD_STATUSES.find(s=>s.id===lead.status)||LEAD_STATUSES[0];
return (
setSelectedLead(lead)} style={{background:"white",border:`1px solid ${C.borderLight}`,borderRadius:10,padding:"14px 18px",cursor:"pointer"}}
onMouseEnter={e=>{e.currentTarget.style.borderColor=C.tealPale;}}
onMouseLeave={e=>{e.currentTarget.style.borderColor=C.borderLight;}}>
{lead.email&&
✉ {lead.email}
}
{lead.source==="wetravel"?"🌐 WeTravel":"📱 Social"}
{lead.dateAdded}
{lead.notes&&
{lead.notes}
}
);
})}
{filtered.length===0&&
No leads match this filter.
}
{selectedLead&&(
e.target===e.currentTarget&&setSelectedLead(null)} style={{position:"fixed",inset:0,background:"rgba(8,40,30,0.5)",zIndex:500,display:"flex",alignItems:"center",justifyContent:"center"}}>
{selectedLead.name}
setSelectedLead(null)} style={{width:26,height:26,borderRadius:6,border:`1px solid ${C.borderLight}`,background:"none",cursor:"pointer",fontSize:13,color:C.textMuted}}>✕
{[["Email",selectedLead.email||"Not provided"],["Source",selectedLead.source==="wetravel"?"🌐 WeTravel":"📱 Social"],["Date Added",selectedLead.dateAdded],["Notes",selectedLead.notes||"—"]].map(([l,v])=>(
))}
Update Status
{LEAD_STATUSES.map(s=>(
updateStatus(selectedLead.id,s.id)} style={{padding:"6px 12px",borderRadius:20,border:`1px solid ${s.color}44`,background:selectedLead.status===s.id?s.color+"22":"white",color:s.color,fontFamily:"inherit",fontSize:11,cursor:"pointer",fontWeight:selectedLead.status===s.id?500:400}}>{s.label}
))}
setSelectedLead(null)}>Close
)}
{showAddModal&&(
e.target===e.currentTarget&&setShowAddModal(false)} style={{position:"fixed",inset:0,background:"rgba(8,40,30,0.5)",zIndex:500,display:"flex",alignItems:"center",justifyContent:"center"}}>
setShowAddModal(false)} onAdd={lead=>{syncUp([...leads,lead]);setShowAddModal(false);toast("Lead added!");}} tripName={trip.name}/>
)}
{showCSV&&(
e.target===e.currentTarget&&setShowCSV(false)} style={{position:"fixed",inset:0,background:"rgba(8,40,30,0.5)",zIndex:500,display:"flex",alignItems:"center",justifyContent:"center"}}>
Import WeTravel CSV
Export your participant or booking list from WeTravel as a CSV and upload it here. The portal auto-detects name, email, date, and payment status columns.
fileRef.current&&fileRef.current.click()}>
📂
Click to upload a CSV file from WeTravel
Accepts .csv exports from WeTravel participant or booking reports
Or paste CSV content directly
{csvPreview.length>0&&(
Preview — {csvPreview.length} leads detected
{csvPreview.map((l,i)=>(
{l.name}
{l.email||"No email"}
🌐 WeTravel
s.id===l.status)?.label||l.status} color={LEAD_STATUSES.find(s=>s.id===l.status)?.color||C.textMuted}/>
))}
)}
{setShowCSV(false);setCsvText("");setCsvPreview([]);}}>Cancel
Import {csvPreview.length>0?`${csvPreview.length} Leads`:""}
)}
);
}
// ─── SALES PIPELINE ───────────────────────────────────────────────────────────
function SalesPipeline({currentUser,toast}) {
const [activeTab,setActiveTab] = useState("sponsored");
const [leadsGroup,setLeadsGroup] = useState(INIT_LEADS_GROUP);
const [leadsCustom,setLeadsCustom] = useState(INIT_LEADS_CUSTOM);
const [sponsoredTrips,setSponsoredTrips] = useState(INIT_SPONSORED_TRIPS);
const [showAddLead,setShowAddLead] = useState(false);
const [selectedLead,setSelectedLead] = useState(null);
const [activeSponsoredTrip,setActiveSponsoredTrip] = useState(null);
const [filterStatus,setFilterStatus] = useState("all");
if (!currentUser.pipelineAccess) {
return (
🔒
Access Restricted
The Sales Pipeline is visible to Growth & Partnerships, Travel Operations Leads, Brand & Storytelling Lead, Chief of Staff Office, and CEO only.
);
}
const isLeadTab = activeTab==="external"||activeTab==="custom";
const leads = activeTab==="external"?leadsGroup:leadsCustom;
const stages = activeTab==="external"?PIPELINE_STAGES_GROUP:PIPELINE_STAGES_CUSTOM;
const setLeads = activeTab==="external"?setLeadsGroup:setLeadsCustom;
const filtered = filterStatus==="all"?leads:leads.filter(l=>l.status===filterStatus);
const byStage = stages.reduce((acc,s)=>{acc[s.id]=leads.filter(l=>l.status===s.id);return acc;},{});
const convRate = leads.length>0?Math.round((leads.filter(l=>l.status==="booked"||l.status==="feepaid"||l.status==="proposal").length/leads.length)*100):0;
const needsFollowup = leads.filter(l=>l.status==="lead"||l.status==="outreach"||l.status==="emailsent").length;
const updateLeadStatus=(id,status)=>{setLeads(ls=>ls.map(l=>l.id===id?{...l,status}:l));toast("Lead status updated!");};
const totalSponsoredSeats = sponsoredTrips.reduce((a,t)=>a+t.totalSeats,0);
const totalBooked = sponsoredTrips.reduce((a,t)=>a+t.bookedSeats,0);
const totalPending = sponsoredTrips.reduce((a,t)=>a+t.depositPending,0);
const totalSocialLeads = sponsoredTrips.reduce((a,t)=>a+(t.leads||[]).filter(l=>l.source==="social").length,0);
const totalSocialBooked = sponsoredTrips.reduce((a,t)=>a+(t.leads||[]).filter(l=>l.source==="social"&&l.status==="booked").length,0);
const totalWTLeads = sponsoredTrips.reduce((a,t)=>a+(t.leads||[]).filter(l=>l.source==="wetravel").length,0);
const totalWTBooked = sponsoredTrips.reduce((a,t)=>a+(t.leads||[]).filter(l=>l.source==="wetravel"&&l.status==="booked").length,0);
const socialConv = totalSocialLeads>0?Math.round((totalSocialBooked/totalSocialLeads)*100):0;
const wtConv = totalWTLeads>0?Math.round((totalWTBooked/totalWTLeads)*100):0;
const TABS = [{id:"sponsored",label:"TWWU Sponsored Trips"},{id:"external",label:"External Group Trips & Retreats"},{id:"custom",label:"Custom Trips"}];
return (
Internal — Role Gated
Sales Pipeline
{isLeadTab&&
setShowAddLead(true)} style={{padding:"8px 16px",borderRadius:8,border:"1px solid rgba(100,201,196,0.4)",background:"rgba(100,201,196,0.1)",color:C.tealPale,fontFamily:"inherit",fontSize:12,cursor:"pointer"}}>+ Add Lead }
{TABS.map(t=>(
{setActiveTab(t.id);setFilterStatus("all");setActiveSponsoredTrip(null);}} style={{padding:"8px 18px",borderRadius:20,border:`1px solid ${activeTab===t.id?"rgba(100,201,196,0.6)":"rgba(255,255,255,0.12)"}`,background:activeTab===t.id?"rgba(100,201,196,0.15)":"transparent",color:activeTab===t.id?C.tealPale:"rgba(255,255,255,0.5)",fontFamily:"inherit",fontSize:12,cursor:"pointer"}}>{t.label}
))}
{activeTab==="sponsored"&&(
activeSponsoredTrip===null?(
{[
{label:"Total Seats Available",value:totalSponsoredSeats,sub:"across all sponsored trips"},
{label:"Seats Filled",value:`${totalBooked} / ${totalSponsoredSeats}`,sub:`${totalSponsoredSeats>0?Math.round((totalBooked/totalSponsoredSeats)*100):0}% capacity filled`},
{label:"Deposits Pending",value:totalPending,sub:"awaiting confirmation"},
{label:"Seats Remaining",value:totalSponsoredSeats-totalBooked-totalPending,sub:"open to book"},
].map(s=>(
{s.label}
{s.value}
{s.sub}
))}
{[
{label:"Social Media Leads",total:totalSocialLeads,booked:totalSocialBooked,conv:socialConv,color:C.tealMid,icon:"📱"},
{label:"WeTravel Bookings Page",total:totalWTLeads,booked:totalWTBooked,conv:wtConv,color:"#7a5520",icon:"🌐"},
].map(src=>(
{[["Leads In",src.total],["Booked",src.booked],["Conv. Rate",`${src.conv}%`]].map(([l,v])=>(
))}
{src.conv}% of leads from this source converted to a booked seat
))}
Trip-by-Trip Seat Status
Click any trip to manage leads
{sponsoredTrips.map(trip=>{
const tripLeads=trip.leads||[];
const pct=Math.round((trip.bookedSeats/trip.totalSeats)*100);
const remaining=trip.totalSeats-trip.bookedSeats-trip.depositPending;
const tSocial=tripLeads.filter(l=>l.source==="social").length;
const tSocialB=tripLeads.filter(l=>l.source==="social"&&l.status==="booked").length;
const tWT=tripLeads.filter(l=>l.source==="wetravel").length;
const tWTB=tripLeads.filter(l=>l.source==="wetravel"&&l.status==="booked").length;
const barColor=pct>=80?C.green:pct>=50?C.tealMid:C.amber;
return (
setActiveSponsoredTrip(trip.id)} style={{background:"white",border:`1px solid ${C.borderLight}`,borderRadius:12,padding:"20px 24px",cursor:"pointer"}}
onMouseEnter={e=>{e.currentTarget.style.borderColor=C.tealPale;e.currentTarget.style.boxShadow="0 4px 16px rgba(34,159,164,0.1)";}}
onMouseLeave={e=>{e.currentTarget.style.borderColor=C.borderLight;e.currentTarget.style.boxShadow="none";}}>
{trip.name}
{trip.destination} · {trip.dates}
{trip.bookedSeats}/{trip.totalSeats}
seats filled
{tripLeads.length} leads →
{[{label:"Confirmed",value:trip.bookedSeats,color:C.green},{label:"Deposit Pending",value:trip.depositPending,color:C.amber},{label:"Open Seats",value:remaining,color:C.textMuted}].map(s=>(
{[{label:"📱 Social",leads:tSocial,booked:tSocialB,conv:tSocial>0?Math.round((tSocialB/tSocial)*100):0},{label:"🌐 WeTravel",leads:tWT,booked:tWTB,conv:tWT>0?Math.round((tWTB/tWT)*100):0}].map(src=>(
{src.label}
{[["Leads",src.leads],["Booked",src.booked],["Conv.",`${src.conv}%`]].map(([l,v])=>(
))}
))}
);
})}
):(
t.id===activeSponsoredTrip)}
onBack={()=>setActiveSponsoredTrip(null)}
onUpdate={updated=>setSponsoredTrips(ts=>ts.map(t=>t.id===updated.id?updated:t))}
toast={toast}
/>
)
)}
{isLeadTab&&(
{[{label:"Total Leads",value:leads.length},{label:"Converted",value:leads.filter(l=>l.status==="booked"||l.status==="feepaid"||l.status==="proposal").length},{label:"Conversion Rate",value:`${convRate}%`},{label:"Needs Follow-Up",value:needsFollowup}].map(stat=>(
{stat.label}
{stat.value}
))}
setFilterStatus("all")} style={{...btn(filterStatus==="all"?"primary":""),fontSize:11}}>All ({leads.length})
{stages.map(s=>(
setFilterStatus(s.id)} style={{...btn(filterStatus===s.id?"outline":""),fontSize:11}}>{s.label} ({byStage[s.id]?.length||0})
))}
{stages.filter(s=>s.id!=="noproc").map(s=>(
{s.label}
{byStage[s.id]?.length||0} leads
{(byStage[s.id]||[]).map(lead=>(
setSelectedLead(lead)} style={{background:C.offWhite,border:`1px solid ${C.borderLight}`,borderRadius:6,padding:"8px 10px",marginBottom:6,cursor:"pointer"}}>
{lead.name}
{lead.tripInterest||lead.tripType}
{lead.ninetyDay&&
⚠ 90-day
}
))}
))}
{activeTab==="custom"&&
🕐 90-Day Rule: Any custom trip departing within 90 days of inquiry requires a governance call decision before proceeding. Flag these for Anina.
}
{filtered.map(lead=>(
setSelectedLead(lead)} style={{background:"white",border:`1px solid ${C.borderLight}`,borderRadius:12,padding:"16px 20px",cursor:"pointer"}}
onMouseEnter={e=>{e.currentTarget.style.borderColor=C.tealPale;e.currentTarget.style.boxShadow="0 4px 16px rgba(34,159,164,0.1)";}}
onMouseLeave={e=>{e.currentTarget.style.borderColor=C.borderLight;e.currentTarget.style.boxShadow="none";}}>
{lead.name}
{lead.ninetyDay&&
⚠ 90-Day }
{lead.tripInterest||lead.tripType} · {lead.source}
{lead.contact&&
✉ {lead.contact}
}
{activeTab==="custom"&&lead.groupSize&&
👥 {lead.groupSize} guests · {lead.travelDates}
}
{(()=>{const stage=stages.find(s=>s.id===lead.status);return stage?
:null;})()}
{lead.lastContact?`Last contact: ${lead.lastContact}`:"Not yet contacted"}
))}
)}
{selectedLead&&(
e.target===e.currentTarget&&setSelectedLead(null)} style={{position:"fixed",inset:0,background:"rgba(8,40,30,0.5)",zIndex:500,display:"flex",alignItems:"center",justifyContent:"center"}}>
{selectedLead.name}
{selectedLead.source} · Received {selectedLead.dateReceived}
setSelectedLead(null)} style={{width:28,height:28,borderRadius:6,border:`1px solid ${C.borderLight}`,background:"none",cursor:"pointer",fontSize:14,color:C.textMuted}}>✕
{[["Contact",selectedLead.contact||"Not provided"],["Source",selectedLead.source],["Trip Interest",selectedLead.tripInterest||selectedLead.tripType||"—"],["Travel Dates",selectedLead.travelDates||"—"],["Group Size",selectedLead.groupSize||"—"],["Design Package",selectedLead.designPackage||"Not selected"]].map(([l,v])=>(
))}
{selectedLead.notes&&
Notes
{selectedLead.notes}
}
Update Status
{stages.map(s=>(
{updateLeadStatus(selectedLead.id,s.id);setSelectedLead(l=>({...l,status:s.id}));}} style={{padding:"6px 12px",borderRadius:20,border:`1px solid ${s.color}44`,background:selectedLead.status===s.id?s.color+"22":"white",color:s.color,fontFamily:"inherit",fontSize:11,cursor:"pointer",fontWeight:selectedLead.status===s.id?500:400}}>{s.label}
))}
setSelectedLead(null)}>Close
toast("Notes saved!")}>Save Notes
)}
{showAddLead&&setShowAddLead(false)} onAdd={lead=>{setLeads(ls=>[lead,...ls]);toast("Lead added!");setShowAddLead(false);}}/>}
);
}
function AddLeadModal({activeTab,onClose,onAdd}) {
const [f,setF] = useState({name:"",contact:"",source:"Social Media",tripInterest:"",tripType:"Group Trips",travelDates:"",groupSize:"",notes:"",ninetyDay:false,designPackage:""});
const set=(k,v)=>setF(p=>({...p,[k]:v}));
const SOURCES=["Social Media","Website Inquiry Form","Referral — Text","Referral — Email","Referral — Call"];
const handleAdd=()=>{
if(!f.name) return;
const id="l_"+Date.now();
const newLead=activeTab==="external"
?{id,name:f.name,contact:f.contact,source:f.source,tripInterest:f.tripInterest||"TBD",status:"lead",assignedTo:"",notes:f.notes,dateReceived:new Date().toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"}),lastContact:""}
:{id,name:f.name,contact:f.contact,source:f.source,tripType:f.tripType,travelDates:f.travelDates,groupSize:f.groupSize,services:[],status:"lead",assignedTo:"",notes:f.notes,dateReceived:new Date().toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"}),lastContact:"",designPackage:f.designPackage,ninetyDay:f.ninetyDay};
onAdd(newLead);
};
return (
e.target===e.currentTarget&&onClose()} style={{position:"fixed",inset:0,background:"rgba(8,40,30,0.5)",zIndex:600,display:"flex",alignItems:"center",justifyContent:"center"}}>
Add Lead — {activeTab==="external"?"External Group Trips":"Custom Trips"}
Notes
{activeTab==="custom"&&
set("ninetyDay",e.target.checked)}/>⚠ Flag as 90-Day rule — trip departs within 90 days of inquiry
}
Cancel
Add Lead
);
}
// ─── PROPOSAL COMPONENTS ──────────────────────────────────────────────────────
function InternalDraftTab({proposal}) {
const [stageData,setStageData] = useState(()=>PROPOSAL_STAGES.reduce((acc,s)=>({...acc,[s.id]:{checklist:{},fields:{},assignedOwner:"",completedAt:"",notes:""}}),{}));
const [activeStage,setActiveStage] = useState("s1");
const [stageStatus,setStageStatus] = useState(()=>PROPOSAL_STAGES.reduce((acc,s)=>({...acc,[s.id]:"pending"}),{}));
const updateField=(sId,k,v)=>setStageData(d=>({...d,[sId]:{...d[sId],fields:{...d[sId].fields,[k]:v}}}));
const updateCheck=(sId,k,v)=>setStageData(d=>({...d,[sId]:{...d[sId],checklist:{...d[sId].checklist,[k]:v}}}));
const updateOwner=(sId,v)=>setStageData(d=>({...d,[sId]:{...d[sId],assignedOwner:v}}));
const markComplete=sId=>{
setStageStatus(s=>({...s,[sId]:"complete"}));
setStageData(d=>({...d,[sId]:{...d[sId],completedAt:new Date().toLocaleString("en-US",{month:"short",day:"numeric",hour:"numeric",minute:"2-digit"})}}));
const idx=PROPOSAL_STAGES.findIndex(s=>s.id===sId);
if(idx
v==="complete").length;
const progress=Math.round((completedCount/PROPOSAL_STAGES.length)*100);
return (
End-to-End Proposal Process
Internal Draft Pipeline
{completedCount}/{PROPOSAL_STAGES.length}
stages complete
{PROPOSAL_STAGES.map(s=>{
const ss=stageStatus[s.id];const sc=stageStatusColor[ss];const isActive=activeStage===s.id;
return (
setActiveStage(s.id)} style={{display:"flex",alignItems:"center",gap:6,padding:"6px 12px",borderRadius:20,border:`1.5px solid ${isActive?C.tealMid:C.borderLight}`,background:isActive?C.tealWash:"white",cursor:"pointer",fontFamily:"inherit",fontSize:11,fontWeight:500,color:isActive?C.tealMid:C.textMuted}}>
{s.num}
{s.title}
{ss==="complete"&&✓ }
);
})}
{PROPOSAL_STAGES.map(stage=>{
if(stage.id!==activeStage) return null;
const sd=stageData[stage.id];const ss=stageStatus[stage.id];
const checkDone=stage.checklist.filter((_,i)=>sd.checklist[i]).length;
return (
{stage.num}
{stage.dept}
{stage.num}. {stage.title}
{stage.status}
Owners: {stage.owners.join(", ")}
{stageStatusLabel[ss]}
{sd.completedAt&&
Completed {sd.completedAt}
}
Stage Checklist
{checkDone}/{stage.checklist.length}
{stage.checklist.map((item,i)=>(
updateCheck(stage.id,i,!sd.checklist[i])} style={{display:"flex",alignItems:"center",gap:10,padding:"10px 12px",borderRadius:8,cursor:"pointer",marginBottom:6,background:sd.checklist[i]?C.greenBg:"white",border:`1px solid ${sd.checklist[i]?"rgba(59,109,17,0.2)":C.borderLight}`}}>
{sd.checklist[i]&&✓ }
{item}
))}
{stage.fields.map(f=>(
{f.label}
{f.type==="textarea"
?
))}
updateOwner(stage.id,e.target.value)} style={{...inputSt,cursor:"pointer",appearance:"none",marginBottom:12}}>
Unassigned
{TEAM.map(m=>{m.name} — {m.role} )}
{sd.assignedOwner&&(()=>{const m=TEAM.find(t=>t.id===sd.assignedOwner);return m?
:null;})()}
{ss==="pending"&&
setStageStatus(s=>({...s,[stage.id]:"inprogress"}))} style={{...btn("outline"),width:"100%",justifyContent:"center",marginBottom:8}}>Start This Stage }
{ss==="inprogress"&&
markComplete(stage.id)} style={{...btn("primary"),width:"100%",justifyContent:"center",marginBottom:8}}>✓ Mark Stage Complete }
{ss==="complete"&&<>
✓
Stage Complete
{sd.completedAt}
setStageStatus(s=>({...s,[stage.id]:"inprogress"}))} style={{...btn(),width:"100%",justifyContent:"center",fontSize:11}}>Reopen Stage >}
Navigate Stages
{const i=PROPOSAL_STAGES.findIndex(s=>s.id===activeStage);if(i>0)setActiveStage(PROPOSAL_STAGES[i-1].id);}} disabled={activeStage===PROPOSAL_STAGES[0].id} style={{...btn(),flex:1,justifyContent:"center",fontSize:11,opacity:activeStage===PROPOSAL_STAGES[0].id?0.4:1}}>← Prev
{const i=PROPOSAL_STAGES.findIndex(s=>s.id===activeStage);if(iNext →
);
})}
);
}
function HotelPackages({selected,onSelect}) {
const [tier,setTier] = useState("luxe");
return (
Hotel Packages
Select one per proposal
{["luxe","gold","silver"].map(t=>(
setTier(t)} style={{padding:"10px 20px",border:"none",background:"none",cursor:"pointer",fontFamily:"inherit",fontSize:12,fontWeight:500,borderBottom:tier===t?`2px solid ${PKG[t].sel}`:"2px solid transparent",color:tier===t?PKG[t].sel:C.textMuted,marginBottom:-1}}>{PKG[t].label}
))}
{HOTELS[tier].map(h=>{
const ps=PKG[tier];const isSel=selected===h.id;
return (
onSelect(h.id)} style={{border:`2px solid ${isSel?ps.sel:ps.border}`,borderRadius:10,overflow:"hidden",cursor:"pointer",boxShadow:isSel?`0 0 0 3px ${ps.bg}`:"none"}}>
{ps.label}
{h.name}
{h.desc}
{h.amenities.map(a=>{a}
)}
{h.price} / person / night
{e.stopPropagation();onSelect(h.id);}} style={{width:"100%",marginTop:8,padding:"7px",borderRadius:5,border:`1px solid ${isSel?C.tealMid:C.borderMid}`,background:isSel?C.tealMid:"white",color:isSel?"white":C.textMid,fontSize:11,fontFamily:"inherit",cursor:"pointer",fontWeight:500}}>{isSel?"Selected ✓":"Select"}
);
})}
);
}
function ItineraryBuilder({days,setDays}) {
const [open,setOpen] = useState([0]);
const toggle=i=>setOpen(o=>o.includes(i)?o.filter(x=>x!==i):[...o,i]);
const update=(i,k,v)=>setDays(d=>d.map((day,idx)=>idx===i?{...day,[k]:v}:day));
return (
Day-by-Day Itinerary
setDays(d=>[...d,{title:"New Day",desc:"",meals:"",hotel:""}])}>+ Add Day
{days.map((day,i)=>(
toggle(i)} style={{display:"flex",alignItems:"center",gap:10,padding:"10px 14px",background:C.warmGray,cursor:"pointer"}}>
Day {i+1}
{e.stopPropagation();update(i,"title",e.target.value);}} onClick={e=>e.stopPropagation()} style={{flex:1,border:"none",background:"transparent",fontFamily:"inherit",fontSize:13,fontWeight:500,color:C.textDark,outline:"none"}}/>
{open.includes(i)?"▲":"▼"}
{open.includes(i)&&(
)}
))}
);
}
function AddOns({selected,onToggle,isClient}) {
return (
{isClient?"Enhance Your Journey":"Optional Add-Ons"}
{isClient?"Tap any to select":`${selected.length} selected`}
{ADDONS.map(a=>{
const isSel=selected.includes(a.id);
return (
onToggle(a.id)} style={{border:`1.5px solid ${isSel?C.tealMid:C.borderLight}`,borderRadius:8,padding:"12px 14px",background:isSel?C.tealWash:"white",cursor:"pointer",position:"relative"}}>
{isClient&&isSel&&
Added
}
{isClient
?<>
{a.icon}
{a.name}
{a.desc}
{a.price}
>
:
{isSel&&✓ }
{a.icon} {a.name}
{a.desc}
{a.price}
}
);
})}
);
}
// ─── PROPOSAL BUILDER ─────────────────────────────────────────────────────────
function ProposalBuilder({proposal,onBack,addNotif,toast,currentUser,setProposals}) {
const p=proposal;
const [activeTab,setActiveTab] = useState("internal");
const [selectedHotel,setSelectedHotel] = useState("l1");
const [selectedAddons,setSelectedAddons] = useState(["a1","a2"]);
const [days,setDays] = useState(DAYS_DEFAULT);
const [status,setStatus] = useState(p.status||"review");
const [commentText,setCommentText] = useState("");
const [comments,setComments] = useState(p.conversations||[]);
const [busy,setBusy] = useState({});
const [handoffLog,setHandoffLog] = useState([]);
const [stageData] = useState(()=>PROPOSAL_STAGES.reduce((acc,s)=>({...acc,[s.id]:{checklist:{},fields:{},assignedOwner:"",completedAt:"",notes:""}}),{}));
const handleExport=()=>{
exportProposalJSON(p,stageData,days,selectedHotel,selectedAddons);
const record={id:Date.now(),destination:p.destination,client:p.client,exportedAt:new Date().toLocaleString("en-US",{month:"short",day:"numeric",year:"numeric",hour:"numeric",minute:"2-digit"}),exportedBy:currentUser.name,fileName:`TWWU_OpsHandoff_${p.destination.replace(/[^a-zA-Z0-9]/g,"_")}_${new Date().toISOString().slice(0,10)}.json`,status:"Sent to Operations"};
setHandoffLog(l=>[record,...l]);
setProposals(ps=>ps.map(pr=>pr.id===p.id?{...pr,status:"handed"}:pr));
setStatus("handed");
addNotif({type:"📦 Handoff Package Exported",detail:`${p.destination} — ${p.client}`,by:`${currentUser.name} (TWWU)`,clientEmail:"",clientNotified:false});
toast("Handoff file downloaded and logged!");
};
const fire=async(key,teamPayload,clientPayload)=>{
setBusy(b=>({...b,[key]:true}));
try {
await notifyTeam({...teamPayload,clientEmail:p.clientEmail});
if(clientPayload&&p.clientEmail) await notifyClient({...clientPayload,clientEmail:p.clientEmail,clientName:p.client,destination:p.destination,dates:p.dates});
addNotif({...teamPayload,type:teamPayload.eventType,by:teamPayload.actionBy,clientEmail:p.clientEmail,clientNotified:!!p.clientEmail&&!!clientPayload});
} finally{setBusy(b=>({...b,[key]:false}));}
};
const handleShare=()=>{
fire("share",{eventType:"📤 Proposal Shared",proposal:`${p.destination} — ${p.dates}`,detail:`Proposal sent to ${p.client} for review.`,actionBy:`${currentUser.name} (TWWU)`},{eventType:"✨ Your Proposal is Ready",detail:`We are excited to share your ${p.destination} proposal.`,curatorName:currentUser.name});
toast(p.clientEmail?`Proposal shared! Notification sent to ${p.clientEmail}`:"Proposal shared!");
};
const handleStatusChange=(newStatus)=>{
setStatus(newStatus);
setProposals(ps=>ps.map(pr=>pr.id===p.id?{...pr,status:newStatus}:pr));
fire("status",{eventType:"📋 Status Updated",proposal:p.destination,detail:`Status changed to: ${STATUS_LABELS[newStatus]}`,actionBy:`${currentUser.name} (TWWU)`},{eventType:"📋 Proposal Update",detail:`Your ${p.destination} proposal status has been updated.`,curatorName:currentUser.name});
toast("Status updated!");
};
const handleSendReply=()=>{
if(!commentText.trim()) return;
const msg=commentText.trim();
setComments(c=>[...c,{name:currentUser.name,role:"team",text:msg,time:"Just now"}]);
fire("reply",{eventType:"💬 Team Reply Sent",proposal:p.destination,detail:msg,actionBy:`${currentUser.name} (TWWU)`},{eventType:"💬 New Message from Your Curator",detail:msg,curatorName:currentUser.name});
toast(p.clientEmail?`Reply sent to ${p.clientEmail}`:"Reply sent!");
setCommentText("");
};
const tabs=[
{id:"internal",label:"🏗 Internal Draft",desc:"Pipeline & working notes"},
{id:"proposal",label:"📄 Proposal Builder",desc:"Client-facing content"},
{id:"conversations",label:"💬 Conversations",desc:`${comments.length} messages — trip-persistent`},
{id:"handoff",label:"📦 Ops Handoff",desc:handoffLog.length>0?`${handoffLog.length} export${handoffLog.length>1?"s":""} logged`:"Export to operations"},
];
return (
← Back to Proposals
{p.destination}
👤 {p.client}
📅 {p.dates}
📋 {p.tripType}
👥 {p.groupSize} guests
{p.clientEmail?
✉ {p.clientEmail} — notifications active
:
No client email
}
{(status==="approved"||status==="handed")&&{status==="handed"?"↓ Re-export Handoff":"↓ Export to Operations"} }
{busy.share?"Sending...":"Share with Client"}
toast("Draft saved!")}>Save Draft
{tabs.map((tab,i)=>(
setActiveTab(tab.id)} style={{flex:1,padding:"14px 12px",border:"none",borderRight:i
{tab.label}
{tab.desc}
))}
{activeTab==="internal"&&
}
{activeTab==="proposal"&&(
🔒 This content is for building the client-facing proposal. Nothing here is visible to the client until you click Share with Client .
{setSelectedHotel(id);toast("Hotel updated!");}}/>
setSelectedAddons(a=>a.includes(id)?a.filter(x=>x!==id):[...a,id])} isClient={false}/>
Proposal Draft
{busy.share?"Sending...":"Send to Client"}
Your {p.destination} Escape
Dear {p.client}, we are delighted to present your exclusive {p.tripType.toLowerCase()} experience, crafted with care for {p.groupSize} guests.
handleStatusChange(e.target.value)} style={{width:"100%",border:`1px solid ${C.borderLight}`,borderRadius:7,padding:"8px 12px",fontFamily:"inherit",fontSize:12,color:C.textDark,outline:"none",background:"white",cursor:"pointer"}}>
Draft
In Review
Feedback Received
Approved
{[["Type",p.tripType],["Group Size",`${p.groupSize} guests`],["Dates",p.dates],["Nights",`${p.nights} nights`]].map(([l,v])=>(
))}
)}
{activeTab==="conversations"&&(
💬 Conversations are trip-persistent — all messages across every phase of this engagement live here.
Conversations
{comments.length} messages
{comments.map((c,i)=>{
const m=c.role==="team"?TEAM.find(t=>t.name===c.name)||{initials:"TM",color:C.tealDeep,bg:C.tealWash}:{initials:p.client.split(" ").map(w=>w[0]).join("").slice(0,2),color:C.amber,bg:C.amberBg};
return (
{m.initials}
{c.name}
{c.role==="team"?"TWWU":"Client"}
{c.time}
{c.text}
);
})}
{comments.length===0&&
No messages yet.
}
{[["Client",p.client],["Email",p.clientEmail||"Not provided"],["Trip",`${p.destination} · ${p.dates}`],["Type",p.tripType],["Group Size",`${p.groupSize} guests`]].map(([l,v])=>(
))}
)}
{activeTab==="handoff"&&(
Export to Operations Portal
Ops Handoff Package
This exports a structured .json file containing the full approved proposal. Upload it directly into the Travel Operations Portal to auto-populate the Journey Manager workspace.
{status!=="approved"&&status!=="handed"&&
⚠ Proposal must be Approved before exporting to Operations.
}
File will include
{[["Proposal details","Client, destination, dates, trip type, group size"],["Full itinerary","All day-by-day entries with meals and hotel"],["Accommodation","Selected hotel package and tier"],["Add-ons","All selected optional experiences"],["Stage completion data","All 7 proposal stages with notes and owners"],["Handoff metadata","Export timestamp, schema version"]].map(([title,desc])=>(
))}
{status==="handed"?"↓ Re-export Handoff Package":status==="approved"?"↓ Export Ops Handoff Package":"Approval Required to Export"}
{(status==="approved"||status==="handed")&&
Upload the downloaded file in the Travel Operations Portal → Import Proposal
}
How to import in Ops Portal
Import Instructions
{[
{step:"01",title:"Download the handoff file",desc:"Click Export above. The file downloads as a .json to your device."},
{step:"02",title:"Open the Travel Operations Portal",desc:"Log in as a Travel Operations Lead."},
{step:"03",title:"Go to Import Proposal",desc:'Select "Import Proposal" from the dashboard or trip workspace.'},
{step:"04",title:"Upload the file",desc:"Select the downloaded .json file. The portal reads the schema and pre-fills all fields."},
{step:"05",title:"Review and assign",desc:"Journey Managers review the imported trip, confirm vendor details, and begin the booking phase."},
].map((s,i)=>(
))}
Handoff Log
{handoffLog.length} export{handoffLog.length!==1?"s":""} recorded
{handoffLog.length===0
?
:handoffLog.map((record,i)=>(
📦
{record.destination} — {record.client}
{record.fileName}
{record.status}
{record.exportedAt} · {record.exportedBy}
))
}
)}
);
}
// ─── PROPOSALS SECTION ────────────────────────────────────────────────────────
function ProposalCard({p,onClick}) {
const sc=STATUS_COLORS[p.status]||STATUS_COLORS.draft;
const assignee=TEAM.find(t=>t.id===p.assignedTo);
return (
{e.currentTarget.style.borderColor=C.tealPale;e.currentTarget.style.boxShadow="0 4px 20px rgba(34,159,164,0.1)";}}
onMouseLeave={e=>{e.currentTarget.style.borderColor=C.borderLight;e.currentTarget.style.boxShadow="none";}}>
{p.destination}
{assignee&&
}
{p.tripType}
{p.client} · {p.dates} · {p.groupSize} guests
{p.clientEmail?`✉ ${p.clientEmail}`:"No client email"}
💬 {p.comments}
{p.activity}
{p.nights} nights
);
}
function NewProposalModal({onClose,onCreate,addNotif,toast,currentUser}) {
const [f,setF] = useState({destination:"",tripType:"Group Trips",clientName:"",clientEmail:"",dates:"",nights:"",groupSize:"",notes:""});
const [emailErr,setEmailErr] = useState("");
const set=(k,v)=>setF(p=>({...p,[k]:v}));
const validateEmail=e=>{if(e&&!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e))setEmailErr("Please enter a valid email address");else setEmailErr("");};
const handleCreate=async()=>{
if(!f.destination||!f.clientName){toast("Please fill in destination and client name.");return;}
if(f.clientEmail&&emailErr){toast("Please fix the client email.");return;}
const newP={id:Date.now().toString(),destination:f.destination,client:f.clientName,clientEmail:f.clientEmail,tripType:f.tripType,groupSize:parseInt(f.groupSize)||1,dates:f.dates||"TBD",nights:parseInt(f.nights)||0,status:"draft",activity:"Just created",comments:0,assignedTo:currentUser.id,conversations:[]};
await notifyTeam({eventType:"🌍 New Proposal Created",proposal:`${f.destination} — ${f.dates||"TBD"}`,clientName:f.clientName,clientEmail:f.clientEmail,detail:`New ${f.tripType} for ${f.clientName}.`,actionBy:`${currentUser.name} (TWWU)`});
addNotif({type:"🌍 New Proposal Created",detail:`${f.destination} for ${f.clientName}`,by:`${currentUser.name} (TWWU)`,clientEmail:f.clientEmail,clientNotified:!!f.clientEmail});
toast(f.clientEmail?`Proposal created! Welcome email sent to ${f.clientEmail} 💛`:"Proposal created!");
onCreate(newP);onClose();
};
return (
e.target===e.currentTarget&&onClose()} style={{position:"fixed",inset:0,background:"rgba(8,40,30,0.5)",zIndex:500,display:"flex",alignItems:"center",justifyContent:"center"}}>
New Proposal
Client Information
Client Name * set("clientName",e.target.value)}/>
Client Email — receives notifications
{set("clientEmail",e.target.value);validateEmail(e.target.value);}}/>
{emailErr&&
{emailErr}
}
{f.clientEmail&&!emailErr&&
✓ Client will receive email notifications
}
Internal Notes Not sent to client
Cancel
{f.clientEmail&&!emailErr?"Create & Notify Client 💛":"Create Proposal"}
);
}
function ProposalsDashboard({proposals,setProposals,onOpen,toast,addNotif,currentUser}) {
const [filterType,setFilterType] = useState("all");
const [showModal,setShowModal] = useState(false);
const groupTrips=proposals.filter(p=>p.tripType==="Group Trips"||p.tripType==="Retreats");
const customTrips=proposals.filter(p=>p.tripType==="Custom Trips"||p.tripType==="Corporate Retreats");
const filtered=filterType==="all"?proposals:proposals.filter(p=>p.tripType===filterType);
return (
Proposals
All active proposals across trip types
setShowModal(true)} style={{padding:"8px 18px",borderRadius:8,border:"1px solid rgba(100,201,196,0.4)",background:"rgba(100,201,196,0.1)",color:C.tealPale,fontFamily:"inherit",fontSize:12,cursor:"pointer"}}>+ New Proposal
{[{label:"Group Trips & Retreats",value:groupTrips.length,sub:"proposals"},{label:"Custom & Corporate",value:customTrips.length,sub:"proposals"},{label:"Pending Approval",value:proposals.filter(p=>p.status==="review").length,sub:"awaiting client"},{label:"Approved",value:proposals.filter(p=>p.status==="approved").length,sub:"this period"}].map(s=>(
{s.label}
{s.value}
{s.sub}
))}
setFilterType("all")} style={{...btn(filterType==="all"?"primary":""),fontSize:11}}>All ({proposals.length})
{TRIP_TYPES.map(t=>(
setFilterType(t)} style={{...btn(filterType===t?"outline":""),fontSize:11}}>{t} ({proposals.filter(p=>p.tripType===t).length})
))}
{filterType==="all"?(
Group Trips & Retreats
{groupTrips.map(p=>
onOpen(p.id)}/>)}
{groupTrips.length===0&&No group trip or retreat proposals yet.
}
Custom Trips & Corporate Retreats
{customTrips.map(p=>
onOpen(p.id)}/>)}
{customTrips.length===0&&No custom trip proposals yet.
}
):(
{filtered.map(p=>
onOpen(p.id)}/>)}
{filtered.length===0&&No proposals for this trip type yet.
}
)}
{showModal&&
setShowModal(false)} onCreate={np=>{setProposals(ps=>[np,...ps]);onOpen(np.id);}} addNotif={addNotif} toast={toast} currentUser={currentUser}/>}
);
}
// ─── CLIENT VIEW ──────────────────────────────────────────────────────────────
function ClientView({proposal,addNotif,toast}) {
const p=proposal||INIT_PROPOSALS[0];
const [pkgTier,setPkgTier] = useState("luxe");
const [selectedAddons,setSelectedAddons] = useState(["a1","a2"]);
const [msgText,setMsgText] = useState("");
const [messages,setMessages] = useState(p.conversations?.length>0?p.conversations:[{name:"Shammy",role:"team",text:"Hi! Excited to share this with you. Do let me know if you have any questions.",time:"Recently"}]);
const sendMsg=async()=>{
if(!msgText.trim()) return;
const msg=msgText.trim();
setMessages(m=>[...m,{name:p.client,role:"client",text:msg,time:"Just now"}]);
await notifyTeam({eventType:"💬 New Client Message",proposal:p.destination,clientName:p.client,clientEmail:p.clientEmail,detail:msg,actionBy:`${p.client} (Client)`});
addNotif({type:"💬 New Client Message",detail:msg,by:`${p.client} (Client)`,clientEmail:p.clientEmail,clientNotified:false});
toast("Message sent to your travel curator!");setMsgText("");
};
const handleApprove=async()=>{
await notifyTeam({eventType:"✅ Client Approved",proposal:p.destination,clientName:p.client,clientEmail:p.clientEmail,detail:`${p.client} has approved the proposal.`,actionBy:`${p.client} (Client)`});
addNotif({type:"✅ Client Approved",detail:`${p.client} approved the ${p.destination} proposal.`,by:`${p.client} (Client)`,clientEmail:p.clientEmail,clientNotified:true});
toast("Proposal approved! Your travel team has been notified.");
};
return (
TWWU
Your Travel Proposal
{p.destination}
A bespoke journey crafted exclusively for you
{[`📅 ${p.dates}`,`👥 ${p.groupSize} Guests`,`🌙 ${p.nights} Nights`].map(pill=>(
{pill}
))}
A Word from Your Travel Curator
Dear {p.client}, we are absolutely delighted to present your exclusive escape to {p.destination}. This proposal has been thoughtfully curated to balance discovery with pure luxury.
Every element has been personally vetted and confirmed with our trusted local partners. Your comfort, privacy, and delight are our highest priority.
Your Accommodation Options
{["luxe","gold","silver"].map(t=>{const ps=PKG[t];return setPkgTier(t)} style={{padding:"9px 20px",borderRadius:20,border:`1.5px solid ${pkgTier===t?ps.sel:C.borderMid}`,background:pkgTier===t?ps.sel:"white",color:pkgTier===t?"white":C.textMid,fontFamily:"inherit",fontSize:12,fontWeight:500,cursor:"pointer"}}>{ps.label} ;})}
{(()=>{const hotels=HOTELS[pkgTier];const ps=PKG[pkgTier];const h=hotels[0];return(
{h.name} — Recommended
{h.desc}
{h.amenities.map(a=>
Also at this tier: {hotels[1].name} and {hotels[2].name} . Ask your curator for details.
);})()}
Enhance Your Journey
setSelectedAddons(a=>a.includes(id)?a.filter(x=>x!==id):[...a,id])} isClient={true}/>
Ready to confirm?
Once you approve, we will begin securing all reservations.
Approve This Proposal
toast("Revision request sent to your curator!")} style={{width:"100%",padding:"9px",borderRadius:6,border:`1px solid ${C.borderMid}`,background:"white",color:C.textMid,fontFamily:"inherit",fontSize:12,cursor:"pointer"}}>Request Changes
{messages.map((m,i)=>(
{m.role==="team"?"SH":p.client.split(" ").map(w=>w[0]).join("").slice(0,2)}
{m.name}
{m.role==="team"?"TWWU":"You"}
{m.time}
{m.text}
))}
);
}
// ─── SIDEBAR ──────────────────────────────────────────────────────────────────
function Sidebar({view,setView,notifCount,onBell,currentUser,onLogout}) {
const nav=[
{id:"dashboard", icon:"⊞", label:"Dashboard"},
{id:"pipeline", icon:"◈", label:"Sales Pipeline", restricted:true},
{id:"finance", icon:"$", label:"Finance & Sales", financeOnly:true},
{id:"proposals", icon:"📄", label:"Proposals", badge:4},
{id:"conversations",icon:"💬",label:"Conversations", badge:2},
{id:"approvals", icon:"✓", label:"Approvals"},
];
return (
The World Within Us
Sales & Proposal Portal
Workspace
{nav.map(item=>{
const locked=(item.restricted&&!currentUser.pipelineAccess)||(item.financeOnly&&!currentUser.financeAccess);
return (
setView(item.id)} style={{display:"flex",alignItems:"center",gap:10,padding:"9px 10px",borderRadius:6,cursor:"pointer",width:"100%",textAlign:"left",border:"none",fontFamily:"inherit",fontSize:13,marginBottom:1,background:view===item.id?"rgba(100,201,196,0.15)":"transparent",color:view===item.id?C.tealPale:locked?"rgba(255,255,255,0.2)":"rgba(255,255,255,0.6)"}}>
{item.icon}
{item.label}
{item.financeOnly&¤tUser.financeAccess&&Private }
{locked&&🔒 }
{item.badge&&!locked&&{item.badge} }
);
})}
🔔
Notifications
{notifCount>0&&{notifCount} }
{currentUser.name}
{currentUser.role}
{currentUser.financeAccess&&
● Finance & Pipeline access
}
Sign out
);
}
// ─── NOTIF PANEL ──────────────────────────────────────────────────────────────
function NotifPanel({open,onClose,notifications}) {
return (
Notifications
Team + client notifications
✕
{DEMO&&
Demo Mode Replace the 4 EmailJS config values at top of file to activate real sending.
}
{notifications.length===0
?
:notifications.map((n,i)=>(
{n.type}
{n.detail}
{n.time} · {n.by}
📬 TWWU {DEMO?"Demo":"✓"}
{n.clientEmail?👤 {n.clientEmail} {DEMO?"Demo":"✓"} :👤 No client email }
))
}
);
}
// ─── DASHBOARD ────────────────────────────────────────────────────────────────
function Dashboard({setView,proposals,onOpen,currentUser}) {
const stats=[
{label:"Active Proposals", value:proposals.filter(p=>p.status!=="approved").length, sub:"across all trip types"},
{label:"Group Trips & Retreats",value:proposals.filter(p=>p.tripType==="Group Trips"||p.tripType==="Retreats").length, sub:"proposals"},
{label:"Custom & Corporate", value:proposals.filter(p=>p.tripType==="Custom Trips"||p.tripType==="Corporate Retreats").length, sub:"proposals"},
{label:"Approved", value:proposals.filter(p=>p.status==="approved").length, sub:"this period"},
];
return (
Welcome back, {currentUser.name.split(" ")[0]}
{currentUser.role} · {currentUser.dept}
{stats.map(st=>(
{st.label}
{st.value}
{st.sub}
))}
{currentUser.pipelineAccess&&(
Sales Pipeline
View lead status across Group Trips and Custom Trips.
setView("pipeline")}>View →
)}
{currentUser.financeAccess&&(
Finance & Sales
Private dashboard — CEO & Chief of Staff only.
setView("finance")}>View →
)}
Recent Proposals
setView("proposals")}>View all →
{proposals.slice(0,4).map(p=>
onOpen(p.id)}/>)}
);
}
function ConversationsView({toast}) {
return (
Open Conversations
Trip-persistent — all phases
{[{initials:"MC",name:"Maria C.",proposal:"Sacred Valley, Peru",time:"2h ago",text:"Can we swap Day 3 for the salt flats experience instead of the weaving workshop?"},{initials:"DH",name:"Dennis Heyman",proposal:"Marrakech, Morocco",time:"Yesterday",text:"The rooftop dinner at Nouba looks incredible. Is that included in the per-person pricing?"}].map((fb,i)=>(
{fb.initials}
{fb.name}
Client
{fb.time} · {fb.proposal}
{fb.text}
toast("Reply sent!")}>Reply
toast("Marked resolved!")}>Mark resolved
))}
);
}
function ApprovalsView({toast}) {
return (
{[{dest:"Marrakech, Morocco",sub:"Dennis Heyman · Shared recently",type:"Group Trips"},{dest:"Sacred Valley, Peru",sub:"Self-Care for Latinas · Updates in progress",type:"Retreats"}].map((p,i)=>(
{p.type}
{p.dest}
{p.sub}
View proposal
{i===0&&toast("Reminder sent!")}>Send reminder }
))}
{[{dest:"Maui, Hawaii",sub:"Amanda & James · Approved March 14, 2026",type:"Custom Trips"},{dest:"Tokyo, Japan",sub:"The Hendersons · Approved March 2, 2026",type:"Custom Trips"}].map((p,i)=>(
))}
);
}
// ─── ROOT ─────────────────────────────────────────────────────────────────────
export default function App() {
const [currentUser,setCurrentUser] = useState(null);
const [view,setView] = useState("dashboard");
const [proposals,setProposals] = useState(INIT_PROPOSALS);
const [activeId,setActiveId] = useState(null);
const [notifOpen,setNotifOpen] = useState(false);
const [notifications,setNotifications] = useState([]);
const [toastMsg,setToastMsg] = useState("");
const toastTimer = useRef(null);
const toast=msg=>{setToastMsg(msg);if(toastTimer.current)clearTimeout(toastTimer.current);toastTimer.current=setTimeout(()=>setToastMsg(""),4000);};
const addNotif=n=>{const time=new Date().toLocaleString("en-US",{month:"short",day:"numeric",hour:"numeric",minute:"2-digit"});setNotifications(p=>[{...n,time},...p]);};
const handleOpen=id=>{setActiveId(id);setView("proposal-detail");};
const handleLogout=()=>{setCurrentUser(null);setView("dashboard");setActiveId(null);};
const activeProposal=proposals.find(p=>p.id===activeId)||proposals[0];
const titles={dashboard:"Dashboard",pipeline:"Sales Pipeline",finance:"Finance & Sales Dashboard",proposals:"Proposals",conversations:"Conversations",approvals:"Approvals","proposal-detail":"Proposal Builder"};
if(!currentUser) return
;
if(currentUser.isClient){
return (
The World Within Us · Sales & Proposal Portal
Sign out
p.id==="marrakech")} addNotif={addNotif} toast={toast}/>
);
}
return (
{setView(v);setActiveId(null);}} notifCount={notifications.length} onBell={()=>setNotifOpen(o=>!o)} currentUser={currentUser} onLogout={handleLogout}/>
{titles[view]||"Sales & Proposal Portal"}
setNotifOpen(o=>!o)} style={{position:"relative",width:32,height:32,borderRadius:6,border:`1px solid ${C.borderLight}`,background:"none",cursor:"pointer",display:"flex",alignItems:"center",justifyContent:"center",color:C.textMid}}>
🔔
{notifications.length>0&&{notifications.length} }
{view==="dashboard" &&
}
{view==="pipeline" &&}
{view==="finance" &&}
{view==="proposals" &&}
{view==="conversations" &&}
{view==="approvals" &&}
{view==="proposal-detail" &&setView("proposals")} addNotif={addNotif} toast={toast} currentUser={currentUser} setProposals={setProposals}/>}
setNotifOpen(false)} notifications={notifications}/>
);
}