How to get started
Step 1: Select Run Simulation.
How to get started
Step 2: Select the Province, Sub-province, Town, Category, Industry, and Sub-Industry for your Business.
Step 3: Click on the SUBMIT button.
Province
Sub-Province
Town
Category
Industry
Sub-Industry

Welcome to the Main Dashboard

Operations Requirements

×

Reschedule Consultation

×

What are you costing?

×

Service Per Hour

Tell me your service. Like “Consultation.”
Total fixed costs per month. Like “5000” rands every month.
Say a number. Like “100” means 100 rands every hour.
How many rands you get each month. Like “3000.”
Pay for helper. Like “800” rands every month.
Pay for helper. Like “600” rands every month.
Pay for helper. Like “500” rands every month.
Pay for helper. Like “400” rands every month.
Cost to rent place. Like “1000” rands each month.
Fuel money. Like “300” rands each month.
Wear and tear on tools. Like “200” rands each month.
Cars or trucks cost. Like “500” rands each month.
Office and ads money. Like “150” rands each month.
Insurance money. Like “100” rands each month.
Little bits of cost. Like “50” rands each month.
Wi-Fi money. Like “80” rands each month.
Paying for car loan. Like “400” rands each month.
Fix and oil tools. Like “120” rands each month.
Go-around fuel. Like “100” rands each month.
Cost for material. Like “300” rands each month.
Cost for material. Like “250” rands each month.
Cost for material. Like “200” rands each month.
Cost for material. Like “150” rands each month.
Cost for material. Like “100” rands each month.
×

Your Entry

FieldValue
Metric Per Hour Per Day Per Month
Total Direct Cost 0.00 0.00 0.00
Total Indirect Cost 0.00 0.00 0.00
Combined Cost 0.00 0.00 0.00
Selling Price Per Unit 0.00 0.00 0.00
Gross Profit 0.00 0.00 0.00
×

Break-even Costing

×

Product per SQM

Tell me its name. Like “Bricks.”
Total fixed costs per month. Like “5000” rands every month.
Say a number. Like “R 100” means 100 rands per square metre.
Area you cover in one hour. Like “20” m².
How many rands you get each month. Like “R 3000.”
Pay for helper. Like “R 800” per month.
Pay for helper. Like “R 600” per month.
Pay for helper. Like “R 500” per month.
Pay for helper. Like “R 400” per month.
Cost to rent a place. Like “R 1000” per month.
Fuel money. Like “R 300” per month.
Wear and tear on tools. Like “R 200” per month.
Repayment for car/truck. Like “R 400” per month.
Office & ads money. Like “R 150” per month.
Safety cover. Like “R 100” per month.
Little bits of cost. Like “R 50” per month.
Wi-Fi money. Like “R 80” per month.
Fix & oil tools. Like “R 120” per month.
Go-around fuel. Like “R 100” per month.
Cost for material. Like “R 300” per month.
Cost for material. Like “R 250” per month.
Cost for material. Like “R 200” per month.
Cost for material. Like “R 150” per month.
Cost for material. Like “R 100” per month.
×

Product Per Item

Tell me its name. Like “Nail.”
Total fixed costs per month. Like “5000” rands every month.
Enter a number. “R 2.50” means two and a half rands each.
How many you can do in an hour. Like “100.”
How many rands you earn each month. Like “R 3000.”
Pay for your first helper. Like “R 800” per month.
Pay for second helper. Like “R 600” per month.
Pay for third helper. Like “R 500” per month.
Pay for fourth helper. Like “R 400” per month.
Cost to rent a place. Like “R 1000” per month.
Fuel money. Like “R 300” per month.
Tools wear and tear. Like “R 200” per month.
Repayment for car/truck. Like “R 400” per month.
Office & ads money. Like “R 150” per month.
Safety cover. Like “R 100” per month.
Little bits of cost. Like “R 50” per month.
Wi-Fi money. Like “R 80” per month.
Fix & oil tools. Like “R 120” per month.
Go-around fuel. Like “R 100” per month.
Cost for material. Like “R 300” per month.
Cost for material. Like “R 250” per month.
Cost for material. Like “R 200” per month.
Cost for material. Like “R 150” per month.
Cost for material. Like “R 100” per month.
Tap to change language
' ); w.document.close(); return w; } function showPdfInTab(win, blob){ if (!blob) return; var url = URL.createObjectURL(blob); try { if (win && !win.closed) { win.location.href = url; // show PDF viewer try { win.focus(); } catch(e) {} } else { window.open(url, '_blank'); } } catch (e) { try { window.open(url, '_blank'); } catch(_) {} } // avoid memory leaks (give the viewer time to load) setTimeout(function(){ try { URL.revokeObjectURL(url); } catch(e) {} }, 60000); } function buildRoadmapPrompt(inputs){ return ( `You are writing a Business Startup Roadmap report as JSON ONLY (no markdown, no extra text). The PDF will follow a Word-template look: Heading 1 (20pt, blue #2F5496), Subtitle (14pt, gray #595959), body (11–12pt). Audience: first-time founder. Tone: practical, encouraging, clear. USER INPUTS: - Location: ${inputs.location} - Business type: ${inputs.business_type} - Stage: ${inputs.stage} - Goal: ${inputs.business_goal} REQUIRED STRUCTURE (match these keys exactly): { "title": "Business Startup Roadmap", "subtitle": "Business type — Location (Province)", "intro": ["paragraph...", "..."], // 4–6 short paragraphs "startup_checklist": { "ask_yourself": ["question...", "..."], // 5–8 items "steps": ["Step 1: ...", "..."] // exactly 9 steps below }, "next_actions": ["Step 1: ...", "..."], // exactly same 9 steps "steps": [ { "heading": "Why", "subtitle": "Assessing the Viability of Your Business Idea", "body": ["paragraph...", "..."], "checkbox_title": "Select all that apply:", "checkboxes": ["...", "..."], // 4–8 items "actions": ["action...", "..."] // 4–8 items }, { "heading": "Determine If There’s a Market", "subtitle": "Validate demand and define your customer", "body": ["..."], "actions": ["..."] }, { "heading": "Understand the Competitive Landscape", "subtitle": "Map competitors and your advantage", "body": ["..."], "actions": ["..."] }, { "heading": "Determine Financial Feasibility", "subtitle": "Costs, pricing, cash flow, break-even", "body": ["..."], "actions": ["..."] }, { "heading": "Explore Funding Options", "subtitle": "Choose the best financing path", "body": ["..."], "actions": ["..."] }, { "heading": "Set Up the Business Properly", "subtitle": "Legal, compliance, operations", "body": ["..."], "tools_required": ["..."], // 0–8 items "equipment_required": ["..."], // 0–8 items "actions": ["..."] }, { "heading": "Build Awareness and Marketing Strategy", "subtitle": "Acquire customers consistently", "body": ["..."], "target_market": ["..."], // 0–8 items "actions": ["..."] }, { "heading": "Plan for Hiring and Staffing", "subtitle": "Roles, timing, systems", "body": ["..."], "actions": ["..."] }, { "heading": "Reduce Business Risk", "subtitle": "Protect the business and plan for shocks", "body": ["..."], "actions": ["..."] } ] } Hard constraints: - steps list must contain exactly these 9 step titles (in this order): 1) Step 1: Clarify your “Why” 2) Step 2: Confirm There’s a Market 3) Step 3: Understand Your Competition 4) Step 4: Determine Financial Feasibility 5) Step 5: Explore Funding Options (If Needed) 6) Step 6: Set Up the Business Properly 7) Step 7: Build Awareness 8) Step 8: Plan for Hiring (If Applicable) 9) Step 9: Reduce Your Risk - Output MUST be valid JSON (double quotes, no trailing commas).` ); } function collectRoadmapInputs(){ var bt = getSubName(); if(!bt) throw new Error('Please select a Business Type first.'); var stage = 'New business (planning/starting)'; try { var toggles = $all('.toggle-bar__btn'); if (toggles && toggles.length >= 2) { var existingBtn = toggles[1]; var isExisting = existingBtn.classList.contains('active') || existingBtn.classList.contains('is-active') || existingBtn.getAttribute('aria-pressed') === 'true' || existingBtn.classList.contains('selected'); if (isExisting) stage = 'Existing business (operating/growing)'; } } catch(e){} var town = getTownName(); return { location: town || '', business_type: bt, stage: stage, business_goal: (stage.indexOf('Existing') > -1) ? 'Grow and improve profitability' : 'Launch a sustainable business' }; } function getRoadmapStageInfo(){ // Mirror the same "existing vs new" toggle logic as collectRoadmapInputs() var stage = 'New business (planning/starting)'; try { var toggles = $all('.toggle-bar__btn'); if (toggles && toggles.length >= 2) { var existingBtn = toggles[1]; var isExisting = existingBtn.classList.contains('active') || existingBtn.classList.contains('is-active') || existingBtn.getAttribute('aria-pressed') === 'true' || existingBtn.classList.contains('selected'); if (isExisting) stage = 'Existing business (operating/growing)'; } } catch(e){} var goal = (stage.indexOf('Existing') > -1) ? 'Grow and improve profitability' : 'Launch a sustainable business'; return { stage: stage, business_goal: goal }; } function getQueryParam(name){ try { return new URLSearchParams(window.location.search).get(name); } catch(e){ return null; } } // Preferred: server builds inputs FROM user_{uid}_new_business_history and calls OpenAI in JSON mode function callOpenAIRoadmapFromHistory(){ var url = window.location.pathname + '?action=roadmap_generate_from_history'; var stageInfo = getRoadmapStageInfo(); var historyId = null; try { historyId = (window.__AUTO_HISTORY && window.__AUTO_HISTORY.id) ? window.__AUTO_HISTORY.id : null; } catch(e){} if (!historyId) { var qp = getQueryParam('history_id'); if (qp) historyId = parseInt(qp, 10) || null; } function collectDashboardPayloadSafe(){ try{ var payload = { cards: {}, charts: {}, meta: {} }; // Cards (ranges) payload.cards.turnover_per_month = bx_getTextById('turnoverRange') || ''; payload.cards.salary_per_month = bx_getTextById('salaryRange') || ''; payload.cards.profit_per_month = bx_getTextById('profitRange') || ''; payload.cards.startup_cost = bx_getTextById('startupCostRange') || ''; // Charts (Chart.js data) var chartIds = ['probabilityChart','marketInfluencesChart','topTownsChart','incomeChart','revenueProfitChart','topProvincesChart']; for (var i=0;i pageH - margin) { doc.addPage(); return margin; } return y; } function addWrapped(doc, text, x, y, maxW, fontSize, lineH){ doc.setFontSize(fontSize); var lines = doc.splitTextToSize(String(text||''), maxW); doc.text(lines, x, y); return y + lines.length * lineH; } function addBullets(doc, items, x, y, maxW, fontSize, lineH){ doc.setFontSize(fontSize); (items || []).forEach(function(it){ var bullet = '• ' + it; var lines = doc.splitTextToSize(bullet, maxW); doc.text(lines, x, y); y += lines.length * lineH; }); return y; } // Render a long-form step block (multi-line text) with light styling. // Supports "__PAGE_BREAK__" token lines to force page breaks (smart: no blank pages). function renderLongFormSteps(doc, rawText, x, y, maxW, margin, pageH, pageW){ var lines = String(rawText || '').split(/\r?\n/); var baseFont = 12; var lineH = 16; function ensure(minSpace){ try { y = ensurePageSpace(doc, y, minSpace, margin, pageH); } catch(e) {} return y; } lines.forEach(function(line){ var t = String(line || '').replace(/\s+$/,''); if (!t.trim()){ y += 8; return; } if (t.trim() === '__PAGE_BREAK__'){ // Avoid inserting an empty page if we are already at the top of a fresh page if (y <= (margin + 0.1)) return; doc.addPage(); y = margin; pageW = doc.internal.pageSize.getWidth(); pageH = doc.internal.pageSize.getHeight(); maxW = pageW - (margin*2); return; } if (t.trim() === '__DASHBOARD_SNAPSHOT_REF__'){ // Insert the reference-style Financial & Market Snapshot (cards + Top Towns + inline insights) try { // ensure we start on a fresh page if (y > (margin + 0.1)) { doc.addPage(); y = margin; pageW = doc.internal.pageSize.getWidth(); pageH = doc.internal.pageSize.getHeight(); maxW = pageW - (margin*2); } var __assets = (typeof globalThis !== 'undefined' && globalThis.__BX_SNAPSHOT_ASSETS) ? globalThis.__BX_SNAPSHOT_ASSETS : null; var __expl = (typeof globalThis !== 'undefined' && globalThis.__BX_CHART_EXPLAINS) ? globalThis.__BX_CHART_EXPLAINS : null; addDashboardSnapshotReference(doc, margin, __expl, __assets); // after the snapshot pages, reset cursor to top of last page pageW = doc.internal.pageSize.getWidth(); pageH = doc.internal.pageSize.getHeight(); maxW = pageW - (margin*2); y = margin; } catch(e){} return; } // Headings var isStep = /^Step\s+\d+\s*:/i.test(t.trim()); if (isStep){ ensure(28); doc.setTextColor(39, 83, 145); doc.setFontSize(18); doc.text(t.trim(), x, y); doc.setTextColor(0); y += 22; return; } // Subhead labels var isLabel = /^(Goal:|Key questions to answer|Practical guidance|Readiness checklist \(select all that apply\)|Next actions)/i.test(t.trim()); if (isLabel){ ensure(22); doc.setTextColor(0); doc.setFontSize(13); doc.setFont(undefined, 'bold'); doc.text(t.trim(), x, y); doc.setFont(undefined, 'normal'); y += 18; return; } // List-like lines var isListish = /^I\s+have\b|^I\s+can\b|^I\s+know\b|^Write\b|^Draft\b|^Create\b|^Decide\b|^Set\s+\d+\s+success\b|^Interview\b|^Define\b|^Collect\b|^Run\b|^Choose\b|^Map\b|^Shortlist\b/i.test(t.trim()); doc.setTextColor(0); doc.setFontSize(baseFont); var prefix = isListish ? '• ' : ''; var wrapped = doc.splitTextToSize(prefix + t.trim(), maxW); ensure(wrapped.length * lineH + 8); doc.text(wrapped, x, y); y += wrapped.length * lineH; }); return y; } // Mixed list: use a checkbox for items matching predicate(text), bullets for the rest. // Used for DB-driven "Why" questions (checkboxes) while keeping other questions as bullets. function addBulletsAndCheckboxes(doc, items, x, y, maxW, fontSize, lineH, predicate){ doc.setFontSize(fontSize); (items || []).forEach(function(it){ var text = String(it || '').trim(); if (!text) return; var useBox = false; try { useBox = predicate ? !!predicate(text) : false; } catch(e) { useBox = false; } if (useBox) { var boxSize = 10; doc.rect(x, y - boxSize + 2, boxSize, boxSize); var lines = doc.splitTextToSize(text, maxW - (boxSize + 8)); doc.text(lines, x + boxSize + 8, y); y += Math.max(lines.length * lineH, boxSize + 6); } else { var bullet = '• ' + text; var lines2 = doc.splitTextToSize(bullet, maxW); doc.text(lines2, x, y); y += lines2.length * lineH; } }); return y; } function addCheckboxes(doc, items, x, y, maxW, fontSize, lineH){ doc.setFontSize(fontSize); (items || []).forEach(function(it){ var boxSize = 10; doc.rect(x, y - boxSize + 2, boxSize, boxSize); var lines = doc.splitTextToSize(String(it||''), maxW - (boxSize + 8)); doc.text(lines, x + boxSize + 8, y); y += Math.max(lines.length * lineH, boxSize + 6); }); return y; } function addFooterPageNumbers(doc){ try{ var pageCount = doc.getNumberOfPages(); for (var i=1;i<=pageCount;i++){ doc.setPage(i); var pageW = doc.internal.pageSize.getWidth(); var pageH = doc.internal.pageSize.getHeight(); doc.setFontSize(10); doc.setTextColor(120); doc.text('Page ' + i + ' of ' + pageCount, pageW - 40, pageH - 20, { align: 'right' }); } doc.setTextColor(0); }catch(e){} } // ─────────────────────────────────────────────────────────────────────────── // Industry "Table-first" pages (match Tables.docx styling) // Uses server-returned industry_bundle pulled from sub_industry_* tables // ─────────────────────────────────────────────────────────────────────────── function bx_safeArr(v){ return (v && Array.isArray(v)) ? v : []; } function bx_val(v){ return (v === null || v === undefined) ? '' : String(v); } function bx_fmtMoney(v){ try{ var n = bx_parseCurrencyNumber(v); if (!isFinite(n)) return bx_val(v); return 'R ' + Math.round(n).toLocaleString('en-ZA'); }catch(e){ return bx_val(v); } } function bx_drawBadge(doc, text, x, y){ // Grey pill label (e.g., "sub_industry_locations") try{ var t = bx_val(text); doc.setFontSize(9); doc.setTextColor(80); doc.setDrawColor(210); doc.setFillColor(245); var w = Math.min(520, doc.getTextWidth(t) + 18); doc.roundedRect(x, y-12, w, 16, 4, 4, 'FD'); doc.text(t, x + 9, y); doc.setTextColor(0); return y + 10; }catch(e){ try { doc.setFontSize(9); doc.setTextColor(80); doc.text(bx_val(text), x, y); doc.setTextColor(0); } catch(_e){} return y + 8; } } function bx_drawSectionHeader(doc, num, title, x, y){ // Blue numbered square + black title (Word-like) try{ var n = String(num || ''); doc.setFillColor(47,84,150); doc.roundedRect(x, y-14, 18, 18, 3, 3, 'F'); doc.setTextColor(255); doc.setFontSize(11); doc.text(n, x + 9, y, { align: 'center' }); }catch(e){} try{ doc.setTextColor(0); doc.setFontSize(14); doc.text(bx_val(title), x + 26, y); }catch(e){} doc.setTextColor(0); return y + 20; } // Simple header to match the Word-template "section number + italic title + underline" // Used for pages like: "2 REQUIRED TOOLS (NON-NEGOTIABLE)" function bx_drawSimpleSectionHeader(doc, num, title, x, y, maxW){ try{ var n = String(num || '').trim(); doc.setTextColor(0); doc.setFontSize(9); doc.setFont(undefined, 'normal'); doc.text(n, x, y); doc.setFont(undefined, 'italic'); doc.text(bx_val(title), x + 18, y); doc.setFont(undefined, 'normal'); // underline across the page (light grey) doc.setDrawColor(190); doc.setLineWidth(0.6); doc.line(x, y + 5, x + maxW, y + 5); doc.setDrawColor(0); doc.setLineWidth(0.2); }catch(e){} doc.setTextColor(0); try{ doc.setFont(undefined, 'normal'); }catch(e){} return y + 16; } // Add the "REQUIRED TOOLS (NON-NEGOTIABLE)" page (table + OpenAI commentary), matching the uploaded example. function bx_addRequiredToolsNonNegotiablePage(doc, margin, industryBundle){ try{ if(!industryBundle) return; var tools = bx_safeArr(industryBundle.tools); if(!tools.length) return; // Narrative is produced by OpenAI as industry_bundle.commentary.tools (server attaches it) var commentary = ''; try{ if(industryBundle.commentary && industryBundle.commentary.tools) commentary = String(industryBundle.commentary.tools || ''); }catch(e){} commentary = (commentary || '').trim(); bx_addPage(doc, 'portrait'); var pageW = doc.internal.pageSize.getWidth(); var pageH = doc.internal.pageSize.getHeight(); var maxW = pageW - margin*2; var y = margin; // Header (matches template) y = bx_drawSimpleSectionHeader(doc, 2, 'REQUIRED TOOLS (NON-NEGOTIABLE)', margin, y, maxW); // Table var cols = [ { key:'tool_name', label:'tool_name', wPct: 2.2, align:'left' }, { key:'quantity', label:'quantity', wPct: 1.0, align:'left' }, { key:'required', label:'required', wPct: 1.0, align:'left' }, { key:'notes', label:'notes', wPct: 3.2, align:'left' } ]; var rows = []; for (var i=0;i= 0) hasTotal = true; if (name && name.toLowerCase().indexOf('total') < 0){ minSum += minC; maxSum += maxC; } rows.push({ equipment: name, min_cost: bx_fmtMoney(minC), max_cost: bx_fmtMoney(maxC) }); } if(!hasTotal){ rows.push({ equipment: 'Total equipment', min_cost: bx_fmtMoney(minSum), max_cost: bx_fmtMoney(maxSum) }); } bx_addPage(doc, 'portrait'); var pageW = doc.internal.pageSize.getWidth(); var pageH = doc.internal.pageSize.getHeight(); var maxW = pageW - margin*2; var y = margin; y = bx_drawSimpleSectionHeader(doc, 5, 'EQUIPMENT — COST RANGE', margin, y, maxW); var cols = [ { key:'equipment', label:'equipment', wPct: 2.2, align:'left' }, { key:'min_cost', label:'min_cost (ZAR)', wPct: 1.4, align:'left' }, { key:'max_cost', label:'max_cost (ZAR)', wPct: 1.4, align:'left' } ]; y = bx_drawWordStyleTable(doc, margin, y, maxW, cols, rows, { fontSize: 10, lineH: 12, padX: 6, padY: 5, margin: margin }); if(commentary){ y += 10; y = ensurePageSpace(doc, y, 60, margin, pageH); doc.setTextColor(0); doc.setFontSize(10); var lines = doc.splitTextToSize(commentary, maxW); doc.text(lines, margin, y); y += lines.length * 14; } doc.setTextColor(0); }catch(e){} } // Add the "STARTUP COST MODEL" page (3 compact tables), matching the uploaded example. function bx_addStartupCostModelPage(doc, margin, industryBundle){ try{ if(!industryBundle) return; var oneOff = bx_safeArr(industryBundle.one_off_costs); var firstCycle = bx_safeArr(industryBundle.first_cycle_inputs); var totals = bx_safeArr(industryBundle.total_startup_cost); if(!oneOff.length && !firstCycle.length && !totals.length) return; var commentary = ''; try{ if(industryBundle.commentary && industryBundle.commentary.startup_costs) commentary = String(industryBundle.commentary.startup_costs || ''); }catch(e){} commentary = (commentary || '').trim(); bx_addPage(doc, 'portrait'); var pageW = doc.internal.pageSize.getWidth(); var pageH = doc.internal.pageSize.getHeight(); var maxW = pageW - margin*2; var y = margin; y = bx_drawSimpleSectionHeader(doc, 6, 'STARTUP COST MODEL', margin, y, maxW); // Helper to draw a small table title function _subTitle(txt){ try{ doc.setFontSize(10); doc.setFont(undefined, 'bold'); doc.text(txt, margin, y); doc.setFont(undefined, 'normal'); y += 10; }catch(e){} } // One-Off Costs if(oneOff.length){ _subTitle('One-Off Costs'); var cols1 = [ { key:'cost_item', label:'cost_item', wPct: 2.2, align:'left' }, { key:'min_cost', label:'min_cost (ZAR)', wPct: 1.4, align:'left' }, { key:'max_cost', label:'max_cost (ZAR)', wPct: 1.4, align:'left' } ]; var rows1 = []; for (var i=0;i pageH - margin){ bx_addPage(doc, 'portrait'); y = margin; } } // top rule ensure(40); doc.setDrawColor(220); doc.line(x, y, x + tableW, y); // header doc.setFontSize(fontSize); doc.setTextColor(0); try{ doc.setFont('helvetica','bold'); }catch(e){} var headerLines = []; var maxHeaderLines = 1; for (var c=0;c maxHeaderLines) maxHeaderLines = lines.length; } var headerH = maxHeaderLines * lineH + padY*2; ensure(headerH + 10); var cx = x; for (var c=0;c maxLines) maxLines = lines.length; } var rowH = maxLines * lineH + padY*2; ensure(rowH + 6); // row text var cx = x; for (var c=0;c= 0){ // Start on a fresh portrait page try { bx_addPage(doc, 'portrait'); } catch(e){ try { doc.addPage(); } catch(_e){} } var pageW = doc.internal.pageSize.getWidth(); var maxW = pageW - margin*2; var y = (typeof startY === 'number' && startY > 0) ? (startY + 28) : margin; // Header doc.setTextColor(0); doc.setFontSize(14); try{ doc.setFont('helvetica','bold'); }catch(e){} doc.text('Mushroom Cultivation Unit', margin, y); try{ doc.setFont('helvetica','normal'); }catch(e){} y += 18; // Section title doc.setTextColor(47,84,150); doc.setFontSize(18); doc.text('Best Locations (Top 5) — Practical Ranking', margin, y); y += 20; // Key point (exact) doc.setTextColor(0); y = addWrapped(doc, 'Key point: the best location is about risk control, not prestige. Your goal is stable production, hygiene control, and fast delivery to buyers.', margin, y, maxW, 11, 14 ); y += 8; // Table label doc.setTextColor(0); doc.setFontSize(12); try{ doc.setFont('helvetica','bold'); }catch(e){} doc.text('Location Ranking Table', margin, y); try{ doc.setFont('helvetica','normal'); }catch(e){} y += 12; var locsTop = [ { rank_no:1, location_type:'Near reliable water sources', risk_level:'Low', affordability:'Medium', market_access:'Medium', key_reason:'Water security + hygiene stability' }, { rank_no:2, location_type:'Peri-urban smallholdings', risk_level:'Medium', affordability:'Medium', market_access:'High', key_reason:'Close to markets + labour availability' }, { rank_no:3, location_type:'Rural farmland', risk_level:'Medium', affordability:'High', market_access:'Medium', key_reason:'Lower land cost (needs delivery plan)' }, { rank_no:4, location_type:'Near transport corridors', risk_level:'Medium', affordability:'Medium', market_access:'High', key_reason:'Fast logistics for retailers/restaurants' }, { rank_no:5, location_type:'Cooperative / community land', risk_level:'Medium', affordability:'High', market_access:'Medium', key_reason:'Shared resources (needs strict rules)' } ]; // Table try { y = bx_drawWordStyleTable(doc, margin, y, maxW, [ { key:'rank_no', label:'Rank', wPct:0.6, align:'center' }, { key:'location_type', label:'Location Type', wPct:1.6 }, { key:'risk_level', label:'Risk Level', wPct:0.9, align:'center' }, { key:'affordability', label:'Affordability', wPct:1.1, align:'center' }, { key:'market_access', label:'Market Access', wPct:1.2, align:'center' }, { key:'key_reason', label:'Main Reason', wPct:2.6 } ], locsTop, { fontSize:10, lineH:12, margin:margin }); } catch(e) {} // Interpretation (exact) y += 16; doc.setTextColor(47,84,150); doc.setFontSize(13); doc.text('How to Interpret the Rankings', margin, y); y += 14; doc.setTextColor(0); y = addWrapped(doc, '1) Near reliable water sources (Best option)', margin, y, maxW, 11, 14); y = addWrapped(doc, 'This is the lowest-risk location choice because mushrooms require consistent water for:', margin, y, maxW, 11, 14); // plain lines (no bullets) var indent = margin + 12; doc.setFontSize(11); doc.text('humidification', indent, y); y += 14; doc.text('sanitation and cleaning', indent, y); y += 14; doc.text('contamination prevention', indent, y); y += 14; doc.text('consistent production cycles', indent, y); y += 16; y = addWrapped(doc, 'If water is unreliable, everything collapses — yield drops, contamination rises, and you lose repeat buyers.', margin, y, maxW, 11, 14); y += 6; y = addWrapped(doc, '2) Peri-urban smallholdings (Strong for sales + labour)', margin, y, maxW, 11, 14); y = addWrapped(doc, 'Peri-urban setups score high because:', margin, y, maxW, 11, 14); doc.text('buyers are closer (fresh delivery matters)', indent, y); y += 14; doc.text('labour is easier to source', indent, y); y += 14; doc.text('transport costs are lower', indent, y); y += 14; doc.text('you can supply retailers, restaurants, and traders more reliably', indent, y); y += 16; y = addWrapped(doc, 'Mushrooms are perishable — market proximity reduces waste and increases repeat orders.', margin, y, maxW, 11, 14); y += 6; y = addWrapped(doc, '3) Rural farmland (Cheaper land, but plan logistics)', margin, y, maxW, 11, 14); y = addWrapped(doc, 'Rural sites can reduce rent and land costs, but they introduce extra risk:', margin, y, maxW, 11, 14); doc.text('higher delivery costs', indent, y); y += 14; doc.text('longer travel times', indent, y); y += 14; doc.text('need for quicker packaging + cold/fast delivery', indent, y); y += 14; doc.text('higher chance of losses if distribution isn’t organised', indent, y); y += 16; y = addWrapped(doc, 'Rural works best when you already have buyers or a delivery route.', margin, y, maxW, 11, 14); y += 6; y = addWrapped(doc, '4) Near transport corridors (Best for distribution models)', margin, y, maxW, 11, 14); y = addWrapped(doc, 'If your business model involves supplying multiple towns, retailers, or restaurants, transport corridor locations improve:', margin, y, maxW, 11, 14); doc.text('delivery speed', indent, y); y += 14; doc.text('route efficiency', indent, y); y += 14; doc.text('consistent supply to repeat buyers', indent, y); y += 14; doc.text('scalability as order volume grows', indent, y); y += 16; y = addWrapped(doc, 'This is ideal for regional distribution plans.', margin, y, maxW, 11, 14); y += 6; y = addWrapped(doc, '5) Cooperative/community land (Works only with clear rules)', margin, y, maxW, 11, 14); y = addWrapped(doc, 'Cooperative/community land can work because shared resources reduce costs, but success depends on strict agreements for:', margin, y, maxW, 11, 14); doc.text('hygiene standards', indent, y); y += 14; doc.text('storage access', indent, y); y += 14; doc.text('water use control', indent, y); y += 16; // Force the last two lines onto the next page (matches the provided layout split) try { bx_addPage(doc, 'portrait'); } catch(e){ try { doc.addPage(); } catch(_e){} } pageW = doc.internal.pageSize.getWidth(); maxW = pageW - margin*2; y = margin; doc.setTextColor(0); doc.setFontSize(11); doc.text('security responsibility', indent, y); y += 14; doc.text('clean/dirty zone boundaries', indent, y); y += 16; y = addWrapped(doc, 'If rules are unclear, contamination and conflict become major risks.', margin, y, maxW, 11, 14); y += 10; doc.setTextColor(47,84,150); doc.setFontSize(13); doc.text('Location Decision Rule (Simple & Effective)', margin, y); y += 14; doc.setTextColor(0); y = addWrapped(doc, 'Choose the site that gives you: Water reliability first Clean workflow space (easy to separate clean vs dirty) Fast buyer access (or strong delivery plan)', margin, y, maxW, 11, 14); y = addWrapped(doc, 'Because in mushrooms: Consistency beats size. Reliability beats speed. Process beats hype.', margin, y, maxW, 11, 14); return true; } }catch(e){} var locs = bx_safeArr(bundle.locations); if (!locs.length) return false; // Start on a fresh portrait page try { bx_addPage(doc, 'portrait'); } catch(e){ try { doc.addPage(); } catch(_e){} } var pageW = doc.internal.pageSize.getWidth(); var maxW = pageW - margin*2; var y = margin; // Header line (business + town if available) try { var sub = (typeof getSubName==='function') ? (getSubName()||'') : ''; var town = (typeof getTownName==='function') ? (getTownName()||'') : ''; var hdr = (sub && town) ? (sub + ' — ' + town) : (sub || town || ''); if (hdr) { doc.setTextColor(0); doc.setFontSize(14); try{ doc.setFont('helvetica','bold'); }catch(e){} doc.text(hdr, margin, y); try{ doc.setFont('helvetica','normal'); }catch(e){} y += 18; } } catch(e){} // Section title doc.setTextColor(47,84,150); doc.setFontSize(18); doc.text('Best Locations (Top 5) — Practical Ranking', margin, y); y += 20; // Key point doc.setTextColor(0); y = addWrapped(doc, 'Key point: Choose a location type that matches your customer access, affordability and risk.', margin, y, maxW, 11, 14 ); y += 8; // Table label doc.setTextColor(0); doc.setFontSize(12); try{ doc.setFont('helvetica','bold'); }catch(e){} doc.text('Location Ranking Table', margin, y); try{ doc.setFont('helvetica','normal'); }catch(e){} y += 12; // Trim to top 5 & normalize rank var locsTop = locs.slice(0, 5).map(function(r, i){ if (r && typeof r === 'object'){ var o = Object.assign({}, r); if (o.rank_no === undefined || o.rank_no === null || o.rank_no === '') o.rank_no = (i+1); return o; } return { rank_no:(i+1), location_type:String(r||''), risk_level:'', affordability:'', market_access:'', key_reason:'' }; }); // Table try { y = bx_drawWordStyleTable(doc, margin, y, maxW, [ { key:'rank_no', label:'Rank', wPct:0.6, align:'center' }, { key:'location_type', label:'Location Type', wPct:1.6 }, { key:'risk_level', label:'Risk', wPct:0.9, align:'center' }, { key:'affordability', label:'Affordability', wPct:1.1, align:'center' }, { key:'market_access', label:'Market Access', wPct:1.2, align:'center' }, { key:'key_reason', label:'Key Reason', wPct:2.6 } ], locsTop, { fontSize:10, lineH:12, margin:margin }); } catch(e) {} // Short interpretation bullets (optional) y += 16; doc.setTextColor(47,84,150); doc.setFontSize(13); doc.text('How to Interpret the Rankings', margin, y); y += 14; doc.setTextColor(0); doc.setFontSize(11); var tips = [ 'Use “Market Access” to estimate daily foot traffic / customer reach.', 'Use “Affordability” to estimate rent and setup pressure on cash flow.', 'Treat “Risk” as stability and compliance effort — reduce risk if you are a first-time founder.', 'Pick the top-ranked option that best matches your target customer and budget.' ]; try { y = addBullets(doc, tips, margin, y, maxW, 11, 14); } catch(e) {} return true; } function bx_addIndustryTablesSection(doc, margin, bundle, roadmap){ if (!bundle || typeof bundle !== 'object') return; var hasAny = (bundle.available === true) || (bx_safeArr(bundle.locations).length + bx_safeArr(bundle.tools).length + bx_safeArr(bundle.equipment).length + bx_safeArr(bundle.one_off_costs).length + bx_safeArr(bundle.first_cycle_inputs).length + bx_safeArr(bundle.total_startup_cost).length + bx_safeArr(bundle.rules).length > 0); if (!hasAny) return; var titleName = bundle.sub_industry_name || (bundle.meta && bundle.meta.sub_industry_name) || 'Sub-Industry'; var emoji = '📊'; try{ var cat = (bundle.meta && (bundle.meta.category || bundle.meta.sub_industry_category)) ? String(bundle.meta.category || bundle.meta.sub_industry_category).toLowerCase() : ''; if (cat.indexOf('agri')>=0 || cat.indexOf('farm')>=0) emoji='🌱'; if (cat.indexOf('food')>=0) emoji='🍽️'; if (cat.indexOf('services')>=0) emoji='🛠️'; if (cat.indexOf('tech')>=0) emoji='💻'; }catch(e){} function newPage(orientation, pageTitle){ bx_addPage(doc, orientation || 'portrait'); var pageW = doc.internal.pageSize.getWidth(); doc.setTextColor(0); doc.setFontSize(16); try{ doc.setFont('helvetica','bold'); }catch(e){} doc.text(pageTitle, margin, margin); doc.setDrawColor(220); doc.line(margin, margin + 8, pageW - margin, margin + 8); try{ doc.setFont('helvetica','normal'); }catch(e){} doc.setTextColor(0); return margin + 28; } // Page title (matches doc feel) var baseTitle = (emoji + ' ' + titleName.toUpperCase() + ' — STRUCTURED FORMAT (TABLE-FIRST)'); // 1) Best Locations (Top 5) var locs = bx_safeArr(bundle.locations); if (locs.length){ var y = newPage('portrait', baseTitle); y = bx_drawSectionHeader(doc, 1, 'BEST LOCATIONS (TOP 5)', margin, y + 6); y = bx_drawBadge(doc, 'sub_industry_locations', margin, y); y += 6; var pageW = doc.internal.pageSize.getWidth(); var tableW = pageW - margin*2; // trim to top 5 var locsTop = locs.slice(0, 5); y = bx_drawWordStyleTable(doc, margin, y, tableW, [ { key:'rank_no', label:'rank', wPct:0.6, align:'center' }, { key:'location_type', label:'location_type', wPct:1.4 }, { key:'risk_level', label:'risk_level', wPct:1.0, align:'center' }, { key:'affordability', label:'affordability', wPct:1.0, align:'center' }, { key:'market_access', label:'market_access', wPct:1.1, align:'center' }, { key:'key_reason', label:'key_reason', wPct:2.9 } ], locsTop, { fontSize:10, lineH:12, margin:margin }); // optional short note if (bundle.commentary && bundle.commentary.locations){ doc.setTextColor(80); doc.setFontSize(10); var lines = doc.splitTextToSize(String(bundle.commentary.locations), tableW); if (lines.length > 6) lines = lines.slice(0,6); doc.text(lines, margin, y + 8); doc.setTextColor(0); } } // 2) Required Tools var tools = bx_safeArr(bundle.tools); if (tools.length){ var y = newPage('portrait', baseTitle); y = bx_drawSectionHeader(doc, 2, 'REQUIRED TOOLS (NON-NEGOTIABLE)', margin, y + 6); y = bx_drawBadge(doc, 'sub_industry_tools', margin, y); y += 6; var pageW = doc.internal.pageSize.getWidth(); var tableW = pageW - margin*2; y = bx_drawWordStyleTable(doc, margin, y, tableW, [ { key:'tool_name', label:'tool_name', wPct:1.8 }, { key:'quantity', label:'quantity', wPct:0.8, align:'center' }, { key:'required', label:'required', wPct:0.9, align:'center' }, { key:'notes', label:'notes', wPct:2.5 } ], tools, { fontSize:10, lineH:12, margin:margin }); if (bundle.commentary && bundle.commentary.tools){ doc.setTextColor(80); doc.setFontSize(10); var lines = doc.splitTextToSize(String(bundle.commentary.tools), tableW); if (lines.length > 6) lines = lines.slice(0,6); doc.text(lines, margin, y + 8); doc.setTextColor(0); } } // 3) Tool Costs var toolCosts = bx_safeArr(bundle.tool_costs); if (toolCosts.length){ var y = newPage('portrait', baseTitle); y = bx_drawSectionHeader(doc, 3, 'TOOLS — COST RANGE', margin, y + 6); y = bx_drawBadge(doc, 'sub_industry_tool_costs', margin, y); y += 6; var pageW = doc.internal.pageSize.getWidth(); var tableW = pageW - margin*2; y = bx_drawWordStyleTable(doc, margin, y, tableW, [ { key:'cost_category', label:'cost_category', wPct:2.2 }, { key:'min_cost', label:'min_cost (ZAR)', wPct:1.0, align:'right', format: bx_fmtMoney }, { key:'max_cost', label:'max_cost (ZAR)', wPct:1.0, align:'right', format: bx_fmtMoney } ], toolCosts, { fontSize:10, lineH:12, margin:margin }); if (bundle.commentary && bundle.commentary.tool_costs){ doc.setTextColor(80); doc.setFontSize(10); var lines = doc.splitTextToSize(String(bundle.commentary.tool_costs), tableW); if (lines.length > 6) lines = lines.slice(0,6); doc.text(lines, margin, y + 8); doc.setTextColor(0); } } // 4) Required Equipment var equip = bx_safeArr(bundle.equipment); if (equip.length){ var y = newPage('portrait', baseTitle); y = bx_drawSectionHeader(doc, 4, 'REQUIRED EQUIPMENT', margin, y + 6); y = bx_drawBadge(doc, 'sub_industry_equipment', margin, y); y += 6; var pageW = doc.internal.pageSize.getWidth(); var tableW = pageW - margin*2; y = bx_drawWordStyleTable(doc, margin, y, tableW, [ { key:'equipment_name', label:'equipment_name', wPct:2.0 }, { key:'required', label:'required', wPct:0.9, align:'center' }, { key:'notes', label:'notes', wPct:2.3 } ], equip, { fontSize:10, lineH:12, margin:margin }); if (bundle.commentary && bundle.commentary.equipment){ doc.setTextColor(80); doc.setFontSize(10); var lines = doc.splitTextToSize(String(bundle.commentary.equipment), tableW); if (lines.length > 6) lines = lines.slice(0,6); doc.text(lines, margin, y + 8); doc.setTextColor(0); } } // 5) Equipment Costs var equipCosts = bx_safeArr(bundle.equipment_costs); if (equipCosts.length){ var y = newPage('portrait', baseTitle); y = bx_drawSectionHeader(doc, 5, 'EQUIPMENT — COST RANGE', margin, y + 6); y = bx_drawBadge(doc, 'sub_industry_equipment_costs', margin, y); y += 6; var pageW = doc.internal.pageSize.getWidth(); var tableW = pageW - margin*2; y = bx_drawWordStyleTable(doc, margin, y, tableW, [ { key:'equipment', label:'equipment', wPct:2.2 }, { key:'min_cost', label:'min_cost (ZAR)', wPct:1.0, align:'right', format: bx_fmtMoney }, { key:'max_cost', label:'max_cost (ZAR)', wPct:1.0, align:'right', format: bx_fmtMoney } ], equipCosts, { fontSize:10, lineH:12, margin:margin }); if (bundle.commentary && bundle.commentary.equipment_costs){ doc.setTextColor(80); doc.setFontSize(10); var lines = doc.splitTextToSize(String(bundle.commentary.equipment_costs), tableW); if (lines.length > 6) lines = lines.slice(0,6); doc.text(lines, margin, y + 8); doc.setTextColor(0); } } // 6) Startup Cost Model var oneOff = bx_safeArr(bundle.one_off_costs); var firstCycle = bx_safeArr(bundle.first_cycle_inputs); var totalCost = bx_safeArr(bundle.total_startup_cost); if (oneOff.length || firstCycle.length || totalCost.length){ var y = newPage('portrait', baseTitle); y = bx_drawSectionHeader(doc, 6, 'STARTUP COST MODEL', margin, y + 6); var pageW = doc.internal.pageSize.getWidth(); var tableW = pageW - margin*2; if (oneOff.length){ y = bx_drawBadge(doc, 'sub_industry_one_off_costs', margin, y); y += 6; doc.setFontSize(12); doc.setTextColor(0); try{ doc.setFont('helvetica','bold'); }catch(e){} doc.text('One-Off Costs', margin, y); try{ doc.setFont('helvetica','normal'); }catch(e){} y += 10; y = bx_drawWordStyleTable(doc, margin, y, tableW, [ { key:'cost_item', label:'cost_item', wPct:2.2 }, { key:'min_cost', label:'min_cost (ZAR)', wPct:1.0, align:'right', format: bx_fmtMoney }, { key:'max_cost', label:'max_cost (ZAR)', wPct:1.0, align:'right', format: bx_fmtMoney } ], oneOff, { fontSize:10, lineH:12, margin:margin }); y += 10; } if (firstCycle.length){ y = bx_drawBadge(doc, 'sub_industry_first_cycle_inputs', margin, y); y += 6; doc.setFontSize(12); doc.setTextColor(0); try{ doc.setFont('helvetica','bold'); }catch(e){} doc.text('First Cycle Inputs', margin, y); try{ doc.setFont('helvetica','normal'); }catch(e){} y += 10; y = bx_drawWordStyleTable(doc, margin, y, tableW, [ { key:'input_item', label:'input_item', wPct:2.2 }, { key:'min_cost', label:'min_cost (ZAR)', wPct:1.0, align:'right', format: bx_fmtMoney }, { key:'max_cost', label:'max_cost (ZAR)', wPct:1.0, align:'right', format: bx_fmtMoney } ], firstCycle, { fontSize:10, lineH:12, margin:margin }); y += 10; } if (totalCost.length){ y = bx_drawBadge(doc, 'sub_industry_total_startup_cost', margin, y); y += 6; doc.setFontSize(12); doc.setTextColor(0); try{ doc.setFont('helvetica','bold'); }catch(e){} doc.text('Total Startup Cost Scenarios', margin, y); try{ doc.setFont('helvetica','normal'); }catch(e){} y += 10; y = bx_drawWordStyleTable(doc, margin, y, tableW, [ { key:'scenario', label:'scenario', wPct:2.2 }, { key:'min_cost', label:'min_cost (ZAR)', wPct:1.0, align:'right', format: bx_fmtMoney }, { key:'max_cost', label:'max_cost (ZAR)', wPct:1.0, align:'right', format: bx_fmtMoney } ], totalCost, { fontSize:10, lineH:12, margin:margin }); } if (bundle.commentary && bundle.commentary.startup_costs){ doc.setTextColor(80); doc.setFontSize(10); var lines = doc.splitTextToSize(String(bundle.commentary.startup_costs), tableW); if (lines.length > 7) lines = lines.slice(0,7); doc.text(lines, margin, y + 10); doc.setTextColor(0); } } // 7) Business Rules var rules = bx_safeArr(bundle.rules); if (rules.length){ var y = newPage('portrait', baseTitle); y = bx_drawSectionHeader(doc, 7, 'BUSINESS RULES', margin, y + 6); y = bx_drawBadge(doc, 'sub_industry_rules', margin, y); y += 6; var pageW = doc.internal.pageSize.getWidth(); var tableW = pageW - margin*2; // normalize to {rule: "..."} var rr = []; for (var i=0;i 7) lines = lines.slice(0,7); doc.text(lines, margin, y + 8); doc.setTextColor(0); } } } // ─────────────────────────────────────────────────────────────────────────── // Dashboard Snapshot (Cards + Charts) for Roadmap PDF // Captures the same "MIN–MAX–AVG" cards and core charts shown on the dashboard. // ─────────────────────────────────────────────────────────────────────────── function bx_getTextById(id){ try { var el = document.getElementById(id); if (!el) return ''; return (el.textContent || '').trim(); } catch(e){ return ''; } } function bx_canvasToPngDataUrl(canvasId, scale){ try { var c = document.getElementById(canvasId); if (!c) return null; if ((c.width || 0) < 2 || (c.height || 0) < 2) return null; var s = (typeof scale === 'number' && scale > 0) ? scale : 2; // Avoid generating extremely large images (memory/perf) var MAX_DIM = 2600; // px var sEffW = Math.min((c.width * s), MAX_DIM) / c.width; var sEffH = Math.min((c.height * s), MAX_DIM) / c.height; var sEff = Math.min(sEffW, sEffH); // If scaling doesn't help, just return the canvas itself if (sEff <= 1.01) return c.toDataURL('image/png', 1.0); var oc = document.createElement('canvas'); oc.width = Math.round(c.width * sEff); oc.height = Math.round(c.height * sEff); var ctx = oc.getContext('2d'); ctx.scale(sEff, sEff); ctx.drawImage(c, 0, 0); return oc.toDataURL('image/png', 1.0); } catch(e){ return null; } } function bx_wait(ms){ return new Promise(function(resolve){ setTimeout(resolve, ms || 0); }); } // Temporarily unhide the dashboard cards + charts so Chart.js can size correctly, then restore. function ensureDashboardSnapshotReady(){ // Ensures all dashboard charts are actually RENDERED (not blank) before we snapshot them into the PDF. // Important: many charts are only created when a user clicks into a dashboard tab; for PDF export we force-load them here. return new Promise(function(resolve){ var minBox = document.getElementById('minMaxContainer'); var grid = document.getElementById('chartsGrid'); // We snapshot these canvases in captureDashboardSnapshotAssets() var chartIds = ['probabilityChart','marketInfluencesChart','topTownsChart','incomeChart','revenueProfitChart','topProvincesChart']; // Track original display styles so we can restore after capture var prev = { minDisp : minBox ? minBox.style.display : null, gridDisp: grid ? grid.style.display : null, boxes : {} }; function getBoxForCanvasId(id){ try{ var el = document.getElementById(id); if(!el) return null; // chart-box is the container used everywhere in the dashboard return el.closest ? el.closest('.chart-box') : null; }catch(e){ return null; } } // Unhide cards + grid + specific chart boxes so Chart.js can measure and paint correctly try { if (minBox) minBox.style.display = 'flex'; if (grid) grid.style.display = 'grid'; chartIds.forEach(function(id){ var box = getBoxForCanvasId(id); if (!box) return; prev.boxes[id] = box.style.display; box.style.display = 'block'; }); // If revenueProfitBox was explicitly hidden, unhide it too (extra safety) var revBox = document.getElementById('revenueProfitBox'); if (revBox){ prev.boxes.__revBox = revBox.style.display; revBox.style.display = 'block'; } // Nudge layout try { window.dispatchEvent(new Event('resize')); } catch(e){} } catch(e){} function safeCall(fn){ try{ if (typeof fn !== 'function') return Promise.resolve(false); var out = fn(); return (out && typeof out.then === 'function') ? out : Promise.resolve(true); }catch(e){ return Promise.resolve(false); } } // Force-load / re-render the charts we include in the PDF snapshot var loads = [ safeCall(loadProbabilityChart), safeCall(loadMarketInfluencesChart), safeCall(loadTopTownsChart), safeCall(loadIncomeChart), safeCall(loadRevenueProfitChart), safeCall(loadTopProvincesChart) ]; // Wait for all loaders (or time out) and then give the browser a beat to paint var TIMEOUT_MS = 4500; var settled = false; function finish(){ // One more resize/update pass to force Chart.js to paint try { window.dispatchEvent(new Event('resize')); } catch(e){} try { if (typeof probChart !== 'undefined' && probChart && probChart.update) probChart.update(); } catch(e){} try { if (typeof infChart !== 'undefined' && infChart && infChart.update) infChart.update(); } catch(e){} try { if (typeof topTownsChartInstance !== 'undefined' && topTownsChartInstance && topTownsChartInstance.update) topTownsChartInstance.update(); } catch(e){} try { if (typeof incomeChartInstance !== 'undefined' && incomeChartInstance && incomeChartInstance.update) incomeChartInstance.update(); } catch(e){} try { if (typeof revenueProfitChartInstance !== 'undefined' && revenueProfitChartInstance && revenueProfitChartInstance.update) revenueProfitChartInstance.update(); } catch(e){} try { if (typeof topProvincesChart !== 'undefined' && topProvincesChart && topProvincesChart.update) topProvincesChart.update(); } catch(e){} bx_wait(450).then(function(){ resolve({ restore: function(){ try { if (minBox) minBox.style.display = prev.minDisp || 'none'; } catch(e){} try { if (grid) grid.style.display = prev.gridDisp || 'none'; } catch(e){} try { chartIds.forEach(function(id){ var box = getBoxForCanvasId(id); if (!box) return; var pd = prev.boxes.hasOwnProperty(id) ? prev.boxes[id] : ''; box.style.display = (pd != null) ? pd : ''; }); var revBox = document.getElementById('revenueProfitBox'); if (revBox && prev.boxes.hasOwnProperty('__revBox')) { revBox.style.display = prev.boxes.__revBox; } } catch(e){} } }); }); } // Timeout safety setTimeout(function(){ if (settled) return; settled = true; finish(); }, TIMEOUT_MS); Promise.allSettled(loads).then(function(){ if (settled) return; settled = true; // Let the browser paint chart canvases after data arrives bx_wait(350).then(finish); }).catch(function(){ if (settled) return; settled = true; finish(); }); }); } function bx_addPage(doc, orientation){ // Robust addPage across jsPDF versions try { var ors = (orientation === 'landscape') ? ['landscape','l'] : ['portrait','p']; for (var i=0;i 6) explLines = explLines.slice(0, 6); explH = explLines.length * explLineH + 10; } // Image area var imgTop = cy + 26; var imgBottom = cy + panelH - (explH ? (explH + 6) : 10); var imgH = Math.max(40, imgBottom - imgTop); var imgW = Math.max(40, panelW - pad*2); if (imgObj && imgObj.data){ try { var iw = imgObj.w || 800; var ih = imgObj.h || 400; var scale = Math.min(imgW / iw, imgH / ih); var w = iw * scale; var h = ih * scale; var ix = cx + (panelW - w)/2; var iy = imgTop + (imgH - h)/2; doc.addImage(imgObj.data, 'PNG', ix, iy, w, h); } catch(e){ try { doc.addImage(imgObj.data, 'PNG', cx + pad, imgTop, imgW, imgH); } catch(_e){} } } else { doc.setTextColor(120); doc.setFontSize(11); doc.text('Chart not available', cx + pad, imgTop + 16); } // Explanation text if (explLines && explLines.length){ doc.setTextColor(80); doc.setFontSize(9); var explY = cy + panelH - explH + 14; doc.text(explLines, cx + pad, explY); } } // Backwards-compatible wrapper function bx_drawChartBox(doc, imgObj, cx, cy, chartW, chartH, label){ return bx_drawChartPanel(doc, imgObj, cx, cy, chartW, chartH, label, ''); } function bx_parseCurrencyNumber(v){ try { if (typeof v === 'number') return v; if (v == null) return NaN; var s = String(v); // Keep digits, decimal point and minus s = s.replace(/[^0-9\.\-]/g, ''); if (!s) return NaN; return parseFloat(s); } catch(e){ return NaN; } } function bx_formatRand(n){ try { if (typeof n !== 'number' || !isFinite(n)) return ''; // Round to whole Rand var x = Math.round(n); var s = String(x); var out = ''; while (s.length > 3){ out = ',' + s.slice(-3) + out; s = s.slice(0, -3); } out = s + out; return 'R ' + out; } catch(e){ return ''; } } function bx_getChartInstance(canvasId){ try { var c = document.getElementById(canvasId); if (!c) return null; if (window.Chart){ // Chart.js v3+ if (typeof Chart.getChart === 'function'){ return Chart.getChart(c); } // Chart.js v2 fallback if (Chart.instances){ for (var k in Chart.instances){ var inst = Chart.instances[k]; if (inst && inst.chart && inst.chart.canvas === c) return inst; if (inst && inst.canvas === c) return inst; } } } return null; } catch(e){ return null; } } function bx_getChartData(canvasId){ try { var inst = bx_getChartInstance(canvasId); if (!inst || !inst.data) return null; var labels = inst.data.labels || []; var datasets = (inst.data.datasets || []).map(function(ds){ return { label: ds.label || '', data: (ds.data || []).slice(0) }; }); return { labels: labels, datasets: datasets }; } catch(e){ return null; } } function bx_buildChartExplanations(){ // Produces short, data-aware explanations for each dashboard chart. var out = {}; // Generic fallback text function generic(title){ return 'This chart summarizes the ' + title.toLowerCase() + ' based on the selected location and industry. Larger values/bars/slices indicate a stronger effect or higher estimate.'; } // Success probability (pie) (function(){ var d = bx_getChartData('probabilityChart'); if (!d || !d.datasets.length || !d.datasets[0].data) { out['probabilityChart'] = generic('success probability'); return; } var labels = d.labels || []; var vals = d.datasets[0].data.map(bx_parseCurrencyNumber); var sum = 0; for (var i=0;i= 0) successIdx = j; if (L.indexOf('fail') >= 0) failIdx = j; } var succ = (successIdx>=0 && isFinite(vals[successIdx])) ? vals[successIdx] : (isFinite(vals[0]) ? vals[0] : 0); var fail = (failIdx>=0 && isFinite(vals[failIdx])) ? vals[failIdx] : (isFinite(vals[1]) ? vals[1] : (sum - succ)); var succPct = Math.round((succ / sum) * 100); var failPct = Math.max(0, 100 - succPct); out['probabilityChart'] = 'Shows the estimated chance of success vs failure for the selected business idea. A higher Success share indicates a stronger outlook. In this estimate: Success ≈ ' + succPct + '% and Failure ≈ ' + failPct + '%.'; })(); // Market influences (donut) (function(){ var d = bx_getChartData('marketInfluencesChart'); if (!d || !d.datasets.length || !d.datasets[0].data) { out['marketInfluencesChart'] = generic('market factors'); return; } var labels = d.labels || []; var vals = d.datasets[0].data.map(function(x){ return isFinite(x) ? x : bx_parseCurrencyNumber(x); }); var pairs = []; var sum = 0; for (var i=0;i=0) { maxDs = d.datasets[i]; break; } if (nm.indexOf('maximum')>=0) { maxDs = d.datasets[i]; break; } } if (!maxDs) maxDs = d.datasets[d.datasets.length-1]; var maxVals = (maxDs.data || []).map(bx_parseCurrencyNumber); var bestI = -1, bestV = -Infinity; for (var j=0;j bestV){ bestV = maxVals[j]; bestI = j; } } var peakMonth = (bestI>=0 && labels[bestI]!=null) ? String(labels[bestI]) : 'the later months'; var peakVal = isFinite(bestV) ? bx_formatRand(bestV) : ''; out['incomeChart'] = 'Projects monthly income potential across the first 12 months. The different series represent conservative to optimistic scenarios. The highest projected month (Maximum scenario) is ' + peakMonth + (peakVal ? (' at about ' + peakVal + '.') : '.'); })(); // Monthly revenue & profit (function(){ var d = bx_getChartData('revenueProfitChart'); if (!d || !d.datasets.length) { out['revenueProfitChart'] = generic('monthly revenue and profit'); return; } var labels = d.labels || []; var ds0 = d.datasets[0] || { label:'', data:[] }; var ds1 = d.datasets[1] || null; function firstLast(arr){ var a = arr.map(bx_parseCurrencyNumber).filter(function(x){return isFinite(x);}); if (!a.length) return null; return { first: a[0], last: a[a.length-1] }; } var rev = firstLast(ds0.data || []); var prof = ds1 ? firstLast(ds1.data || []) : null; var revText = rev ? ('Revenue trends from ' + bx_formatRand(rev.first) + ' to ' + bx_formatRand(rev.last) + '.') : 'Revenue trend is shown over time.'; var profText = prof ? (' Profit trends from ' + bx_formatRand(prof.first) + ' to ' + bx_formatRand(prof.last) + '.') : ''; out['revenueProfitChart'] = 'Shows expected monthly revenue and profit movement over the year. Use the overall direction to plan cash flow and staffing.' + ' ' + revText + profText; })(); // Top provinces (function(){ var d = bx_getChartData('topProvincesChart'); if (!d || !d.labels || !d.labels.length) { out['topProvincesChart'] = generic('top provinces'); return; } var labels = d.labels.map(function(x){ return String(x||''); }).filter(Boolean); var top = labels.slice(0, 3); out['topProvincesChart'] = 'Ranks provinces by suitability for launching this business, using the model’s location and demand signals. Provinces displayed earlier/highest ranked are better candidates. Top provinces shown are: ' + (top.length ? top.join(', ') : 'the first bars') + '.'; })(); // Cards note out['__cards'] = 'The MIN–MAX–AVG figures are scenario ranges. Use AVG as your planning baseline, MIN as a conservative stress-test, and MAX as an optimistic upside case.'; return out; } function bx_explShort(explains, key){ try{ if (!explains) return ''; var v = explains[key]; if (!v && key === '__cards') v = explains['cards']; if (!v) return ''; if (typeof v === 'string') return String(v); if (v.short_summary) return String(v.short_summary); if (v.summary) return String(v.summary); if (v.what_it_shows) return String(v.what_it_shows); if (v.overview) return String(v.overview); return ''; } catch(e){ return ''; } } function bx_explLong(explains, key){ try{ if (!explains) return ''; var v = explains[key]; if (!v && key === '__cards') v = explains['cards']; if (!v) return ''; if (typeof v === 'string') return String(v); var parts = []; if (v.what_it_shows) parts.push('What it shows: ' + String(v.what_it_shows)); if (v.what_it_suggests) parts.push('What it suggests: ' + String(v.what_it_suggests)); if (v.consulting_notes) parts.push(String(v.consulting_notes)); var acts = v.recommended_actions || v.actions; if (Array.isArray(acts) && acts.length){ var bullets = acts.slice(0, 6).map(function(a){ return '- ' + String(a); }).join('\n'); parts.push('Recommended actions:\n' + bullets); } return parts.join('\n\n').trim(); } catch(e){ return ''; } } function bx_captureCanvasObj(canvasId, scale){ try { var c = document.getElementById(canvasId); if (!c) return null; if ((c.width || 0) < 2 || (c.height || 0) < 2) return null; var data = bx_canvasToPngDataUrl(canvasId, scale || 2); if (!data) return null; return { data: data, w: c.width * ((scale && scale>1) ? Math.min(scale, 2) : 1), h: c.height * ((scale && scale>1) ? Math.min(scale, 2) : 1) }; } catch(e){ return null; } } function bx_captureChartAsPngObj(canvasId, scale){ // Robust chart capture: // 1) Try raw canvas toDataURL (fast) // 2) If canvas is hidden/zero-sized, render the existing Chart.js instance onto an offscreen canvas and export try { var obj = bx_captureCanvasObj(canvasId, scale || 2); if (obj && obj.data) return obj; } catch(e){} try { if (!window.Chart) return null; var inst = null; try { inst = bx_getChartInstance(canvasId); } catch(e){ inst = null; } if (!inst || !inst.data) return null; // Build an offscreen canvas so layout/visibility doesn't matter var tmp = document.createElement('canvas'); var W = 900, H = 420; tmp.width = W; tmp.height = H; tmp.style.position = 'absolute'; tmp.style.left = '-12000px'; tmp.style.top = '0px'; tmp.style.pointerEvents = 'none'; tmp.style.opacity = '0'; document.body.appendChild(tmp); var type = null; try { type = (inst.config && inst.config.type) ? inst.config.type : null; } catch(e){ type = null; } if (!type) type = 'bar'; var data = null, opts = null; try { data = JSON.parse(JSON.stringify(inst.data || {})); } catch(e){ data = inst.data || {}; } try { opts = JSON.parse(JSON.stringify(inst.options || {})); } catch(e){ opts = {}; } // Make it deterministic and exportable try { opts.responsive = false; opts.maintainAspectRatio = false; opts.animation = false; } catch(e){} var off = null; try{ off = new Chart(tmp.getContext('2d'), { type: type, data: data, options: opts }); try { off.resize(); } catch(e){} try { off.update('none'); } catch(e){ try { off.update(); } catch(_e){} } }catch(e){ try { if (off && off.destroy) off.destroy(); } catch(_e){} try { tmp.remove(); } catch(_e){} return null; } var dataUrl = null; try { dataUrl = tmp.toDataURL('image/png', 1.0); } catch(e){ dataUrl = null; } try { if (off && off.destroy) off.destroy(); } catch(e){} try { tmp.remove(); } catch(e){} if (!dataUrl) return null; var s = (scale && scale>1) ? Math.min(scale, 2) : 1; return { data: dataUrl, w: W * s, h: H * s }; } catch(e){ return null; } } function captureDashboardSnapshotAssets(){ // Capture cards + canvases into data-URIs so we can insert the snapshot later (e.g., under Step 4). try{ var cards = {}; ['turnoverRange','salaryRange','profitRange','startupCostRange'].forEach(function(id){ cards[id] = bx_getTextById(id) || ''; }); var ids = ['probabilityChart','marketInfluencesChart','topTownsChart','incomeChart','revenueProfitChart','topProvincesChart']; var images = {}; ids.forEach(function(id){ // Capture even if the chart is normally hidden (e.g., revenueProfitChart) images[id] = bx_captureChartAsPngObj(id, 2); }); return { cards: cards, images: images }; } catch(e){ return { cards: {}, images: {} }; } } // Adds TWO snapshot pages with readable charts: // - Page A (landscape): cards + 2 charts side-by-side (probability + factors) // - Page B (landscape): 2 charts stacked full-width (top towns + monthly income) function addDashboardSnapshotPages(doc, margin, explainsOverride, assets){ // ── Page A (cards + 2 charts) var pageW = doc.internal.pageSize.getWidth(); var pageH = doc.internal.pageSize.getHeight(); var maxW = pageW - margin*2; var y = margin; doc.setTextColor(47,84,150); doc.setFontSize(18); doc.text('Financial & Market Snapshot', margin, y); y += 18; doc.setTextColor(90); doc.setFontSize(11); doc.text('Summary metrics and charts based on the selected location and industry.', margin, y); y += 18; // Cards row (4) — wrap the value so it never spills out var gap = 12; var cardW = (maxW - gap*3) / 4; var cardH = 92; var cards = [ { title1:'MIN – MAX – AVG', title2:'TURNOVER / MONTH', valueId:'turnoverRange' }, { title1:'MIN – MAX – AVG', title2:'SALARY / MONTH', valueId:'salaryRange' }, { title1:'MIN – MAX – AVG', title2:'PROFIT / MONTH', valueId:'profitRange' }, { title1:'MIN – MAX – AVG', title2:'STARTUP COST', valueId:'startupCostRange' }, ]; for (var i=0;i 2) { fs = 10; doc.setFontSize(fs); lines = doc.splitTextToSize(String(v), valMaxW); } lines = lines.slice(0, 3); var startY = y + 58; var lh = 12; for (var li=0; li 3) valLines = valLines.slice(0,3); doc.text(valLines, x + cardW/2, y + 44, { align:'center' }); } y += cardH + 14; // Chart header row doc.setTextColor(0); doc.setFontSize(10); doc.text('Financial & Market Snapshot', margin, y); doc.text('Top Towns by Success', margin + 170, y); // right-ish, similar to template y += 8; // Chart box doc.setDrawColor(200); doc.rect(margin, y, maxW, 92); var townsImg = (assets && assets.images && assets.images['topTownsChart']) ? assets.images['topTownsChart'] : bx_captureCanvasObj('topTownsChart', 2); try { if (townsImg) doc.addImage(townsImg, 'PNG', margin+10, y+10, maxW-20, 72); } catch(e){} y += 92 + 14; // Insights text (inline, like the reference image) var combined = ''; try { var c1 = safeStr(bx_explLong(explains,'__cards') || bx_explShort(explains,'__cards') || ''); var c2 = safeStr(bx_explLong(explains,'topTownsChart') || bx_explShort(explains,'topTownsChart') || ''); var c3 = safeStr(bx_explLong(explains,'incomeChart') || bx_explShort(explains,'incomeChart') || ''); combined = (c1 ? c1 : ''); if (c2) combined += (combined ? "\n\n" : "") + c2; if (c3) combined += (combined ? "\n\n" : "") + c3; } catch(e){} combined = safeStr(combined).replace(/\\[object Object\\]/g,'').trim(); if (!combined) combined = safeStr((assets && assets.text && (assets.text.dashboardInsights || assets.text.insights)) ? (assets.text.dashboardInsights || assets.text.insights) : ''); if (!combined) combined = 'Dashboard insights are unavailable.'; // Print with paging doc.setTextColor(0); doc.setFontSize(10); var lh = 12.5; var lines = doc.splitTextToSize(combined, maxW); for (var i=0;i pageH - margin){ bx_addPage(doc, 'portrait'); pageW = doc.internal.pageSize.getWidth(); pageH = doc.internal.pageSize.getHeight(); maxW = pageW - margin*2; y = margin; } doc.text(lines[i], margin, y); y += lh; } } // Adds a compact "Business Success Probability" page that matches the dashboard-style reference: // small title (italic), small chart top-left, then "Interpretation & recommendations" with paragraph + 3 action lines. function bx_addBusinessSuccessProbabilityPage(doc, margin, explains, assets){ try{ // If startY is provided, continue on the current page below the chart section. // Otherwise, start on a fresh portrait page. if (!(typeof startY === 'number' && startY > 0)) { bx_addPage(doc, 'portrait'); } var pageW = doc.internal.pageSize.getWidth(); var pageH = doc.internal.pageSize.getHeight(); // Slightly tighter left margin to mimic the reference screenshot var x = 48; var yTitle = 46; // Title (blue + italic) doc.setTextColor(47,84,150); doc.setFontSize(12); try { doc.setFont('helvetica','italic'); } catch(e){} doc.text('Business Success Probability', x, yTitle); // Chart (top-left, compact) var imgObj = (assets && assets.images && assets.images['probabilityChart']) ? assets.images['probabilityChart'] : bx_captureCanvasObj('probabilityChart', 2); var chartX = x; var chartY = yTitle + 14; var maxChartW = 260; var maxChartH = 150; var drawnH = maxChartH; if (imgObj && imgObj.data){ try{ var iw = imgObj.w || 800; var ih = imgObj.h || 400; var sc = Math.min(maxChartW / iw, maxChartH / ih); var w = iw * sc; var h = ih * sc; doc.addImage(imgObj.data, 'PNG', chartX, chartY, w, h); drawnH = h; }catch(e){ try{ doc.addImage(imgObj.data, 'PNG', chartX, chartY, maxChartW, maxChartH); }catch(_e){} } } var y = chartY + drawnH + 18; // Section heading doc.setTextColor(0); doc.setFontSize(10); try { doc.setFont('helvetica','bold'); } catch(e){} doc.text('Interpretation & recommendations', x, y); y += 14; // Extract OpenAI narrative + actions var notes = ''; var acts = []; try{ var v = (explains && (explains['probabilityChart'] || explains['probability'])) ? (explains['probabilityChart'] || explains['probability']) : null; if (v){ if (typeof v === 'string') { notes = String(v); } else { notes = (v.consulting_notes || v.notes || v.long || v.short_summary || '').toString(); acts = v.recommended_actions || v.actions || []; } } }catch(e){} notes = (notes || '').toString().replace(/\s+/g,' ').trim(); if (!Array.isArray(acts)) acts = []; acts = acts.filter(function(a){ return !!a; }).map(function(a){ return String(a).replace(/^[\-\u2022]\s*/,'').trim(); }).filter(Boolean).slice(0, 3); // Wrap width: intentionally narrower to leave whitespace on the right (like the reference image) var wrapW = Math.min(470, pageW - x - 70); // Fit-to-page (avoid spilling onto a new page) function computeLayout(fs){ var lh = fs + 3; try { doc.setFont('helvetica','bold'); } catch(e){} doc.setFontSize(fs); var lines = doc.splitTextToSize(notes, wrapW); var h = lines.length * lh; if (acts.length){ // blank line + "Recommended actions:" + 3 lines h += lh; // blank h += lh; // label h += acts.length * lh; // actions } return { fs: fs, lh: lh, lines: lines, h: h }; } var layout = computeLayout(9); var bottomLimit = pageH - 54; if (y + layout.h > bottomLimit){ for (var fs = 8.5; fs >= 7; fs -= 0.5){ var t = computeLayout(fs); if (y + t.h <= bottomLimit){ layout = t; break; } } } // Body text (bold, compact) doc.setTextColor(0); try { doc.setFont('helvetica','bold'); } catch(e){} doc.setFontSize(layout.fs); var yy = y; for (var i=0;i bottomLimit){ for (var fs = 8.5; fs >= 7; fs -= 0.5){ var t = computeLayout(fs); if (y + t.h <= bottomLimit){ layout = t; break; } } } // Body text (bold, compact) doc.setTextColor(0); try { doc.setFont('helvetica','bold'); } catch(e){} doc.setFontSize(layout.fs); var yy = y; for (var i=0;i bottomLimit){ for (var fs = 8.5; fs >= 7; fs -= 0.5){ var t = computeLayout(fs); if (y + t.h <= bottomLimit){ layout = t; break; } } } // Body text (bold, compact) doc.setTextColor(0); try { doc.setFont('helvetica','bold'); } catch(e){} doc.setFontSize(layout.fs); var yy = y; for (var i=0;i bottomLimit){ for (var fs = 8.5; fs >= 7; fs -= 0.5){ var t = computeLayout(fs); if (y + t.h <= bottomLimit){ layout = t; break; } } } // Body text (bold, compact) doc.setTextColor(0); try { doc.setFont('helvetica','bold'); } catch(e){} doc.setFontSize(layout.fs); var yy = y; for (var i=0;i pageH - margin){ bx_addPage(doc, 'portrait'); pageW = doc.internal.pageSize.getWidth(); pageH = doc.internal.pageSize.getHeight(); maxW = pageW - margin*2; y = margin; doc.setTextColor(47,84,150); doc.setFontSize(16); doc.text('MIN–MAX–AVG ranges (continued)', margin, y); y += 22; doc.setTextColor(60); doc.setFontSize(11); } doc.text(lines[i], margin, y); y += lh; } } } var charts = [ { key:'probabilityChart', id:'probabilityChart', title:'Business Success Probability' }, { key:'marketInfluencesChart', id:'marketInfluencesChart', title:'Key Market Factors Affecting Success' }, { key:'topTownsChart', id:'topTownsChart', title:'Top Towns by Business Success Probability' }, { key:'incomeChart', id:'incomeChart', title:'Monthly Income Potential' }, { key:'revenueProfitChart', id:'revenueProfitChart', title:'Monthly Revenue & Profit' }, { key:'topProvincesChart', id:'topProvincesChart', title:'Top Provinces to Consider' }, ]; // Optional filter: include only specified charts if (onlyKeys && onlyKeys.length) { charts = charts.filter(function(ch){ return onlyKeys.indexOf(ch.key) >= 0; }); } function writeFlow(lines, x, y, lh, orient){ var pH = doc.internal.pageSize.getHeight(); var pW = doc.internal.pageSize.getWidth(); var mW = pW - margin*2; for (var i=0;i pH - margin){ bx_addPage(doc, orient || 'landscape'); pH = doc.internal.pageSize.getHeight(); pW = doc.internal.pageSize.getWidth(); mW = pW - margin*2; y = margin; } doc.text(lines[i], x, y); y += lh; } return y; } charts.forEach(function(ch){ var longText = bx_explLong(explains, ch.key); if (!longText) return; bx_addPage(doc, 'landscape'); var pW = doc.internal.pageSize.getWidth(); var pH = doc.internal.pageSize.getHeight(); var mW = pW - margin*2; var y = margin; doc.setTextColor(47,84,150); doc.setFontSize(18); doc.text(ch.title, margin, y); y += 20; // Chart image (top half) var imgObj = (assets && assets.images && assets.images[ch.id]) ? assets.images[ch.id] : bx_captureCanvasObj(ch.id, 2); var imgAreaH = Math.max(220, Math.floor((pH - margin*2) * 0.52)); if (imgObj && imgObj.data){ try { var pad = 4; var maxImgW = mW; var maxImgH = imgAreaH; var iw = imgObj.w || 900; var ih = imgObj.h || 450; var scale = Math.min(maxImgW / iw, maxImgH / ih); var w = iw * scale; var h = ih * scale; var ix = margin + (mW - w)/2; var iy = y; doc.addImage(imgObj.data, 'PNG', ix, iy, w, h); y += h + 14; } catch(e){ y += imgAreaH + 14; } } else { doc.setTextColor(120); doc.setFontSize(12); doc.text('Chart image not available.', margin, y + 14); y += imgAreaH + 14; } // Interpretation text doc.setTextColor(30); doc.setFontSize(13); doc.text('Interpretation & recommendations', margin, y); y += 16; doc.setTextColor(60); doc.setFontSize(11); var lines = doc.splitTextToSize(longText, mW); var lh = 15; y = writeFlow(lines, margin, y, lh, 'landscape'); }); } // Adds a compact "Monthly Revenue & Profit" page that matches the dashboard-style reference: // small title (italic), small bar chart top-left, then "Interpretation & recommendations" with narrative + action lines. function bx_addMonthlyRevenueProfitPage(doc, margin, explains, assets){ try{ bx_addPage(doc, 'portrait'); var pageW = doc.internal.pageSize.getWidth(); var pageH = doc.internal.pageSize.getHeight(); // Match the compact reference layout var x = 48; var yTitle = 46; // Title (blue + italic) doc.setTextColor(47,84,150); doc.setFontSize(12); try { doc.setFont('helvetica','italic'); } catch(e){} doc.text('Monthly Revenue & Profit', x, yTitle); // Chart (top-left, compact) — IMPORTANT: ensure it pulls even if the dashboard chart box is hidden var imgObj = (assets && assets.images && assets.images['revenueProfitChart']) ? assets.images['revenueProfitChart'] : null; if (!imgObj) imgObj = bx_captureChartAsPngObj('revenueProfitChart', 2); if (!imgObj) imgObj = bx_captureChartAsPngObj('revenueProfitChartModal', 2); var chartX = x; var chartY = yTitle + 14; var maxChartW = 330; var maxChartH = 145; var drawnH = maxChartH; if (imgObj && imgObj.data){ try{ var iw = imgObj.w || 1000; var ih = imgObj.h || 500; var sc = Math.min(maxChartW / iw, maxChartH / ih); var w = iw * sc; var h = ih * sc; doc.addImage(imgObj.data, 'PNG', chartX, chartY, w, h); drawnH = h; }catch(e){ try{ doc.addImage(imgObj.data, 'PNG', chartX, chartY, maxChartW, maxChartH); }catch(_e){} } } var y = chartY + drawnH + 18; // Section heading doc.setTextColor(0); doc.setFontSize(10); try { doc.setFont('helvetica','bold'); } catch(e){} doc.text('Interpretation & recommendations', x, y); y += 14; // Extract OpenAI narrative + actions var notes = ''; var acts = []; try{ var v = null; if (explains){ v = explains['revenueProfitChart'] || explains['monthlyRevenueProfitChart'] || explains['revenueProfit'] || null; } if (v){ if (typeof v === 'string') { notes = String(v); } else { notes = (v.consulting_notes || v.notes || v.long || v.short_summary || '').toString(); acts = v.recommended_actions || v.actions || []; } } }catch(e){} notes = (notes || '').toString().replace(/\s+/g,' ').trim(); if (!Array.isArray(acts)) acts = []; acts = acts.filter(function(a){ return !!a; }).map(function(a){ return String(a).replace(/^[\-•\u2022]\s*/,'').trim(); }).filter(Boolean).slice(0, 3); // Wrap width: narrower to keep right-side whitespace similar to reference var wrapW = Math.min(470, pageW - x - 70); // Fit-to-page (avoid spilling onto a new page) function computeLayout(fs){ var lh = fs + 3; try { doc.setFont('helvetica','bold'); } catch(e){} doc.setFontSize(fs); var lines = doc.splitTextToSize(notes || '—', wrapW); var h = lines.length * lh; if (acts.length){ h += lh; // blank h += lh; // label h += acts.length * lh; // actions } return { fs: fs, lh: lh, lines: lines, h: h }; } var layout = computeLayout(9); var bottomLimit = pageH - 80; // keep footer breathing room if (y + layout.h > bottomLimit){ for (var fs = 8.5; fs >= 7; fs -= 0.5){ var t = computeLayout(fs); if (y + t.h <= bottomLimit){ layout = t; break; } } } // Body text (bold, compact) doc.setTextColor(0); try { doc.setFont('helvetica','bold'); } catch(e){} doc.setFontSize(layout.fs); var yy = y; for (var i=0;i bottomLimit){ for (var fs = 8.5; fs >= 7; fs -= 0.5){ var t = computeLayout(fs); if (y + t.h <= bottomLimit){ layout = t; break; } } } // Body text (bold, compact) doc.setTextColor(0); try { doc.setFont('helvetica','bold'); } catch(e){} doc.setFontSize(layout.fs); var yy = y; for (var i=0;i pageH - bottom){ doc.addPage(); y = (opts.topMargin||54); } } rows.forEach(function(r){ var left = String(r[0]||''); var right = String(r[1]||''); var leftLines = doc.splitTextToSize(left, leftW); var rightLines = doc.splitTextToSize(right, rightW); var h = Math.max(leftLines.length, rightLines.length) * lh + 8; ensure(h); // grid lines doc.setDrawColor(200); doc.setLineWidth(0.5); doc.line(x, y, x+w, y); doc.line(x, y+h, x+w, y+h); doc.line(x, y, x, y+h); doc.line(x+leftW+10, y, x+leftW+10, y+h); doc.line(x+w, y, x+w, y+h); // text doc.setTextColor(0); doc.text(leftLines, x+4, y+lh); doc.text(rightLines, x+leftW+14, y+lh); y += h; }); // bottom border doc.setDrawColor(200); doc.line(x, y, x+w, y); return y; } function bx_addBusinessRulesAndOverviewsPages(doc, margin, roadmap, reportCostingData, reportForcastingData, reportProjectManagementData, reportSelfMasterData, reportOperationsManagementData, reportManagementData, reportInnovationData, reportHrManagementData, reportBudgetingData, reportGrowthData, reportTimeManagementData, reportSocialMediaData ){ try{ var pageW = doc.internal.pageSize.getWidth(); var pageH = doc.internal.pageSize.getHeight(); var maxW = pageW - margin*2; // -------- BUSINESS RULES page (includes Costing + Forecasting) -------- doc.addPage(); var y = margin; // Title doc.setTextColor(25,60,150); try{ doc.setFont('helvetica','bold'); }catch(e){} doc.setFontSize(16); doc.text('BUSINESS RULES', margin, y); y += 22; doc.setTextColor(0); try{ doc.setFont('helvetica','normal'); }catch(e){} doc.setFontSize(9); var rules = (roadmap && Array.isArray(roadmap.business_rules)) ? roadmap.business_rules : null; if (!rules || !rules.length){ rules = [ 'Set up operations around these rules: water access first, hygiene and biosecurity daily, and simple records for every input and sale. This is what keeps losses low and learning fast.', 'Water access is a primary success driver', 'Biosecurity and hygiene reduce losses', 'Market access matters more than maximum yield', 'Keep simple records of inputs, sales and performance', 'Start small and scale with reinvestment', 'Treat the business rules as daily habits. Do: prioritise water access and plan for interruptions; keep access strict. Do enforce biosecurity and hygiene to reduce losses—make it part of SOPs and training. Do choose market access over chasing maximum yield; reliable delivery and consistent quality win repeat buyers. Do keep simple records of inputs, sales, and performance so you can forecast, price correctly, and improve.', "Don’t scale too fast; start small, prove the workflow and sales, then expand using reinvestment and measured upgrades." ]; } // Render rules as compact bullets/lines y = bx_addRuleBlock(doc, margin, y, maxW, rules, 9); y += 10; // Helper to build "Overview" tables from report_* rows function renderOverview(title, row){ if(!row) return; // section header var h = 18; if (y + h > pageH - margin){ doc.addPage(); y = margin; } try{ doc.setFont('helvetica','bold'); }catch(e){} doc.setFontSize(9); doc.setTextColor(0); doc.text(title + ' (based on your industry selection)', margin, y); y += 12; // match confidence line var cat = bx_num2(bx_pick(row, ['match_confidence_category','category_match_confidence','category_confidence','cat_confidence'], '')); var ind = bx_num2(bx_pick(row, ['match_confidence_industry','industry_match_confidence','industry_confidence','ind_confidence'], '')); var sub = bx_num2(bx_pick(row, ['match_confidence_sub_industry','sub_industry_match_confidence','sub_industry_confidence','sub_confidence'], '')); var mc = 'Match confidence — '; mc += 'Category: ' + (cat!==null?cat.toFixed(3):'—') + ' · '; mc += 'Industry: ' + (ind!==null?ind.toFixed(3):'—') + ' · '; mc += 'Sub-industry: ' + (sub!==null?sub.toFixed(3):'—'); try{ doc.setFont('helvetica','normal'); }catch(e){} doc.setFontSize(8); doc.setTextColor(80); var mcLines = doc.splitTextToSize(mc, maxW); doc.text(mcLines, margin, y); y += mcLines.length*12 + 4; doc.setTextColor(0); // two-column rows var rows = []; function addRow(label, val){ var t = (val===null||val===undefined) ? '' : String(val); if(!t.trim()) return; rows.push([label, t.trim()]); } var base = title.replace(/\s*Overview\s*/i,'').trim(); var baseLower = base.toLowerCase(); function capWords(str){ return String(str||'').replace(/\b[a-z]/g, function(m){ return m.toUpperCase(); }); } var baseCap = capWords(baseLower); // two-column rows var rows = []; function addRow(label, val){ var t = (val===null||val===undefined) ? '' : String(val); if(!t.trim()) return; rows.push([label, t.trim()]); } addRow(baseCap + ' element', bx_pick(row, ['element', baseLower.replace(/\s+/g,'_')+'_element','topic','name','costing_element','forecasting_element'])); addRow('What it means for your business', bx_pick(row, ['what_it_means','meaning','what_it_means_for_your_business','business_meaning'])); addRow('Note', bx_pick(row, ['note','notes'])); addRow('Definition of ' + baseLower, bx_pick(row, ['definition','definition_of','definition_text'])); addRow('Types of ' + baseLower, bx_pick(row, ['types','types_of','types_text'])); addRow('Why ' + baseLower + ' is important', bx_pick(row, ['why_important','why_it_is_important','importance'])); addRow('Impact of ' + baseLower, bx_pick(row, ['impact','impact_of','impact_text'])); addRow('How ' + baseLower + ' is used', bx_pick(row, ['how_used','how_it_is_used','usage'])); addRow('Risks of poor ' + baseLower, bx_pick(row, ['risks_of_poor','risks','risk'])); addRow('Summary', bx_pick(row, ['summary','conclusion'])); // sensible fallbacks to match screenshots if DB column naming differs if(rows.length===0){ // try generic mapping used in many of your report_* tables addRow('Definition', bx_pick(row, ['definition_of_'+title.toLowerCase().replace(/[^a-z]+/g,'_'), 'definition'])); addRow('Types', bx_pick(row, ['types_of_'+title.toLowerCase().replace(/[^a-z]+/g,'_'), 'types'])); addRow('Why it is important', bx_pick(row, ['why_'+title.toLowerCase().replace(/[^a-z]+/g,'_')+'_important', 'why_important'])); addRow('Impact', bx_pick(row, ['impact_of_'+title.toLowerCase().replace(/[^a-z]+/g,'_'), 'impact'])); addRow('How it is used', bx_pick(row, ['how_'+title.toLowerCase().replace(/[^a-z]+/g,'_')+'_is_used', 'how_used'])); addRow('Risks of poor '+title.toLowerCase(), bx_pick(row, ['risks_of_poor_'+title.toLowerCase().replace(/[^a-z]+/g,'_'), 'risks'])); addRow('Summary', bx_pick(row, ['summary'])); } // table width similar to screenshots var leftW = 150; y = bx_addTwoColTable(doc, margin, y, maxW, leftW, rows, {fontSize:8, lineH:12, topMargin:margin, bottomMargin:margin}); y += 12; } // Costing overview and Forecasting overview should appear on the BUSINESS RULES page after rules if space allows var ov = (roadmap && typeof roadmap === 'object' && roadmap.overviews && typeof roadmap.overviews === 'object') ? roadmap.overviews : {}; renderOverview('Costing Overview', ov.costing || reportCostingData); renderOverview('Forecasting Overview', ov.forecasting || reportForcastingData); // -------- Remaining overviews (each flows on pages as needed) -------- renderOverview('Project Management Overview', ov.project_management || reportProjectManagementData); renderOverview('Self Mastering Overview', ov.self_mastering || reportSelfMasterData); renderOverview('Operations Management Overview', ov.operations_management || reportOperationsManagementData); renderOverview('Management Overview', ov.management || reportManagementData); renderOverview('Innovation Overview', ov.innovation || reportInnovationData); renderOverview('HR Management Overview', ov.hr_management || reportHrManagementData); renderOverview('Budgeting Overview', ov.budgeting || reportBudgetingData); renderOverview('Growth Overview', ov.growth || reportGrowthData); renderOverview('Time Management Overview', ov.time_management || reportTimeManagementData); renderOverview('Social Media Overview', ov.social_media || reportSocialMediaData); }catch(e){} } function buildStartupRoadmapPDF(){ // Disabled: legacy PDF generator removed. Route to new RoadmapReport.php if(function_exists('header')){ header('Location: RoadmapReport.php'); exit; } } )(); /* === PATCH: Robust triggers for fbTikTokBox & rule701020Box (visit frequency) === */ (function(){ try { const fbBoxById = document.getElementById('fbTikTokBox'); const fbCanvas = document.getElementById('fbTikTokChart'); const visBox = document.getElementById('rule701020Box'); const visCanvas = document.getElementById('rule701020Chart'); // helper: dispatch to existing handler if available, else run fallback const tryDispatch = (target, fallback) => { try { if (target) { target.dispatchEvent(new Event('click', { bubbles: true })); return true; } } catch (e) {} try { if (typeof fallback === 'function') fallback(); } catch(e){} return false; }; const openFbModalFallback = () => { const ready = (typeof window.facebookTikTokChartInstance !== 'undefined' && window.facebookTikTokChartInstance) ? Promise.resolve() : (typeof window.loadFbTikTokChart === 'function' ? loadFbTikTokChart() : Promise.resolve()); ready.then(() => { try { fbTikTokModal.style.display = 'block'; } catch(e){} try { const ctx = document.getElementById('fbTikTokChartModal').getContext('2d'); if (window.fbTikTokModalChart) fbTikTokModalChart.destroy(); if (window.facebookTikTokChartInstance) { fbTikTokModalChart = new Chart(ctx, { type: 'bar', data: window.facebookTikTokChartInstance.data, options: window.facebookTikTokChartInstance.options || { responsive: true, maintainAspectRatio: false } }); } } catch(e){} }); }; const openVisitModalFallback = () => { const ready = (typeof window.rule701020ChartInstance !== 'undefined' && window.rule701020ChartInstance) ? Promise.resolve() : (typeof window.loadRule701020Chart === 'function' ? loadRule701020Chart() : Promise.resolve()); ready.then(() => { try { visitFreqModal.style.display = 'block'; } catch(e){} try { const ctx = document.getElementById('visitFreqChartModal').getContext('2d'); if (window.visitFreqModalChart) visitFreqModalChart.destroy(); if (window.rule701020ChartInstance) { visitFreqModalChart = new Chart(ctx, { type: 'bar', data: window.rule701020ChartInstance.data, options: window.rule701020ChartInstance.options || { responsive: true, maintainAspectRatio: false } }); } } catch(e){} }); }; const onFbClick = (e) => { try { e.preventDefault(); e.stopPropagation(); } catch(e){} tryDispatch((typeof fbChartBox !== 'undefined' ? fbChartBox : null), openFbModalFallback); }; const onVisClick = (e) => { try { e.preventDefault(); e.stopPropagation(); } catch(e){} tryDispatch((typeof visitChartBox !== 'undefined' ? visitChartBox : null), openVisitModalFallback); }; [fbBoxById, fbCanvas].forEach(el => { if (!el) return; try { el.style.cursor = 'pointer'; } catch(e){} el.addEventListener('click', onFbClick, { passive: true }); el.addEventListener('touchend', onFbClick, { passive: true }); }); [visBox, visCanvas].forEach(el => { if (!el) return; try { el.style.cursor = 'pointer'; } catch(e){} el.addEventListener('click', onVisClick, { passive: true }); el.addEventListener('touchend', onVisClick, { passive: true }); }); } catch (err) { // swallow – non-fatal } })();
\