Great, this is very solid material — and importantly: nothing you pasted is “legacy junk”. It already encodes exactly the patterns we want, just in a different runtime (Apps Script + Google Docs).
What follows is not a rewrite, but a translation + consolidation plan that: • preserves your mental model • fits perfectly into documents2 + TripSection • keeps Tally as a first-class optional intake • avoids premature AI overreach • lets you migrate piece by piece, safely
I’ll structure this in layers, then give you starter code per layer so you can literally save each section as a .txt and come back to it.
⸻
🧭 BIG PICTURE — WHAT YOU ARE BUILDING (REPHRASED)
You are building a self-contained Trip → Document → Offer pipeline, where: 1. Input can come from: • In-app Trip Builder • Tally form (external, fast) • Later: AI-assisted filling 2. Normalization happens inside your app: • TripDraft / Trip entity • Pricing engine • LineItems 3. Document generation is: • deterministic • section-based (not destructive) • extensible (letter, CTA, WhatsApp, etc.) 4. AI enhances after structure exists: • writing • variations • explanation • not core logic
Your Apps Script already does (2) + (3) very well. We are porting that exact logic into your own stack.
⸻
1️⃣ KEY TRANSLATION: GOOGLE DOC → DOCUMENTS2
Let’s map concepts directly.
Apps Script Concept documents2 Equivalent Whole Google Doc Document entity Paragraphs DocSection_TextBlock Headings markdown #, ## Tables DocSection_LineItems or future DocSection_Table Marker text Pinned Section ID Clear above marker Replace specific sections only Preserve WhatsApp CTA Dedicated CTASection Ruta Sheet TripSection.tripData
📌 Critical decision (made for you): 👉 We NEVER “clear the document” in-app We replace or regenerate specific sections by type.
This is safer, reversible, and versionable.
⸻
2️⃣ NEW DOCUMENT TEMPLATE: trip-offer-v1
You were right: this should be a template, not ad-hoc text.
📁 documents2/templates/trip-offer-v1.ts
export const TripOfferTemplate = { key: "trip-offer-v1", label: "Trip Offer / Client Proposal", defaultData: { headerVisible: true, bannerVisible: false, tripVisible: true, lineItemsVisible: true,
contentSections: [
{
id: "intro-letter",
type: "text",
role: "letter-intro",
content: "",
},
{
id: "itinerary-body",
type: "text",
role: "itinerary",
content: "",
},
],
ctaSection: {
enabled: true,
type: "whatsapp",
phone: "",
text:
"Click below to open a WhatsApp chat where we can discuss or schedule a time to talk:",
},
}, };
📌 This replaces: • marker text • destructive clearing • hardcoded structure
⸻
3️⃣ TRIP → DOCUMENT SELF-POPULATOR (IN-APP EQUIVALENT)
This is the direct port of your Apps Script logic.
📁 documents2/lib/populateTripOfferFromTrip.ts
import type { TripDraftData } from "../types/DocTripSection.types";
export function populateTripOfferFromTrip({ trip, pricing, client, }: { trip: TripDraftData; pricing: any; client: { name?: string; email?: string; ref?: string; }; }) { const intro = `
H1Travel Plan / Client Offer
Client: ${client.name || "—"}
Email: ${client.email || "—"}
Reference: ${client.ref || "—"}
Dates: ${trip.startDate || "—"} → ${trip.endDate || "—"}
Participants: ${trip.pax || "—"}
`;
const routeLine = trip.cities .map((c) => c.cityName) .filter(Boolean) .join(" → ");
const itinerary = `
H2Route Plan
${routeLine}
H2Itinerary
${trip.cities .map( (c, i) => `
H3${c.cityName} — ${c.nights} nights
${(c.plans || []).map((p) => - ${p}).join("\n")}
) .join("\n")};
return { contentSections: [ { role: "letter-intro", content: intro.trim(), }, { role: "itinerary", content: itinerary.trim(), }, ], pricingTable: pricing, }; }
📌 This is your TravelPlanDocSelfPopulator, translated cleanly.
⸻
4️⃣ HOW THIS WIRES INTO Documents2Canvas
You already have everything needed.
Add one button in DocSection_TripBuilder:
<button className="micro-btn" onClick={() => onChange?.({ ...section, tripData: { ...trip, _requestPopulateTripOffer: true, }, }) }
📄 Generate Trip Offer Document </button>
Then extend handleTripSectionChange:
if (trip._requestPopulateTripOffer) { const populated = populateTripOfferFromTrip({ trip, pricing: trip.pricing, client: { name: data.headerData?.client, email: data.headerData?.email, ref: data.headerData?.ref, }, });
queueSave({ ...data, contentSections: data.contentSections.map((s) => s.role === "letter-intro" ? { ...s, content: populated.contentSections[0].content } : s.role === "itinerary" ? { ...s, content: populated.contentSections[1].content } : s ), tripSection: { ...next, tripData: { ...trip, _requestPopulateTripOffer: false }, }, });
return; }
💥 Result: • deterministic • repeatable • no AI involved • mirrors your Apps Script behavior
⸻
5️⃣ CTA / WHATSAPP SECTION (MARKER REPLACEMENT)
Instead of marker text:
📁 documents2/components/sections/DocSection_CTA.tsx
export default function DocSection_CTA({ data }) { if (!data?.enabled) return null;
return (
<div className="mt-6 p-4 border rounded bg-green-50 text-center"> <p className="text-sm mb-2">{data.text}</p> <a href={`https://wa.me/${data.phone}`} target="\_blank" className="btn" > 💬 Chat on WhatsApp </a> </div> ); }📌 Cleaner, reusable, editable.
⸻
6️⃣ TALLY FORMS — DECISION & PLAN (IMPORTANT)
You were right to keep Tally.
Decision (made for you):
👉 Tally is an INPUT ADAPTER, not a document generator
Flow:
Tally → Webhook → Normalize → TripDraft → Document
Minimal backend entry point:
📁 api/webhooks/tally/route.ts
import prisma from "lib/prisma";
export async function POST(req: Request) { const payload = await req.json();
const tripDraft = mapTallyToTripDraft(payload);
const doc = await prisma.document.create({ data: { type: "trip-offer-v1", data: { tripSection: { kind: "trip", tripData: tripDraft, }, }, }, });
return Response.json({ ok: true, documentId: doc.id }); }
Mapping stays explicit (like Sheets):
function mapTallyToTripDraft(payload: any) { return { title: payload.trip_name, pax: Number(payload.pax), startDate: payload.start_date, endDate: payload.end_date, cities: payload.cities.map(/_ … _/), }; }
📌 This mirrors your Sheets → Doc logic perfectly.
⸻
7️⃣ WHERE AI FITS (NOT YET, BUT READY)
Only after document exists: • Rewrite intro letter • Generate alternative wording • Explain pricing • Create summary email
Using the DocAIAssistant we already planned.
⸻
8️⃣ YOUR WORK PLAN (COPY THIS)
Week 1 • Add trip-offer-v1 template • Add populateTripOfferFromTrip • Wire button in TripBuilder
Week 2 • CTA section • Tally webhook • Map one Tally form → document
Week 3 • AI assist inside document • Optional AI form filling
⸻
🧠 FINAL THOUGHT (IMPORTANT)
What you built in Apps Script was not a hack — it was a domain-specific document compiler.
You are now rebuilding it: • with versioning • with persistence • with extensibility • with optional AI
You’re doing this in the right order.
When ready, your next concrete step can be as small as:
“Generate trip-offer-v1 template file fully”