// CQ Line Items Extraction // extractLineItems — MUST sync with Deal Room version // Depends on: cq-pricing-data.js window.extractLineItems = (quote) => { const recurringItems = []; const oneTimeItems = []; let recurringSubtotal = 0; let recurringDiscountTotal = 0; let oneTimeSubtotal = 0; let oneTimeDiscountTotal = 0; const locations = quote.locations || []; const payType = quote.paymentType || 'lease'; const numLocations = locations.length; // === EVENT CREDITS - Aggregated by event type + length === const eventAggregates = {}; locations.forEach(loc => { const eventDetails = loc.eventDetails || {}; Object.entries(eventDetails).forEach(([eventId, details]) => { const eventType = EVENT_TYPES.find(e => e.id === eventId); if (!eventType) return; const count = details.count || 0; if (count === 0) return; const length = details.length || 1.5; const vgLevel = details.vgLevel || 'Standard'; const key = `${eventId}-${length}-${vgLevel}`; const baseUnitPrice = details.unitCost ?? getCreditPrice(eventId, length, vgLevel); const unitPrice = details.customPrice ?? details.customCost ?? baseUnitPrice; const discount = details.discount || 0; const discountType = details.discountType || 'percent'; const finalUnitPrice = discountType === 'percent' ? unitPrice * (1 - discount / 100) : Math.max(0, unitPrice - discount); if (!eventAggregates[key]) { eventAggregates[key] = { eventId, eventType, length, vgLevel, totalCount: 0, totalBase: 0, totalFinal: 0 }; } eventAggregates[key].totalCount += count; eventAggregates[key].totalBase += unitPrice * count; eventAggregates[key].totalFinal += finalUnitPrice * count; }); }); // Sort eventAggregates with correct hierarchy: // 1. Produced (first) // 2. Free Produced Promo (right after produced) // 3. Audio Mixing // 4. Free Audio Mixing Promo (right after audio mixing) // 5. Static (any type) // 6. Mobile const sortedEventAggregates = Object.values(eventAggregates).sort((a, b) => { const order = { 'produced': 1, 'freeProducedPromo': 2, 'audioMixing': 3, 'freeAudioMixingPromo': 4, 'staticYearly': 5, 'staticPPU': 6, 'mobileIOS': 7, 'mobileKit': 8 }; const aOrder = order[a.eventId] || 99; const bOrder = order[b.eventId] || 99; return aOrder - bOrder; }); sortedEventAggregates.forEach(agg => { const avgBasePrice = agg.totalBase / agg.totalCount; const avgFinalPrice = agg.totalFinal / agg.totalCount; const totalDiscount = agg.totalBase - agg.totalFinal; const lengthLabel = agg.length >= 1 ? `${agg.length} Hour` : `${Math.round(agg.length * 60)} Minute`; // Check for line item overrides const overrideKey = `event-${agg.eventId}-${agg.length}`; const override = (quote.lineItemOverrides || {})[overrideKey] || {}; let defaultDescription; if (agg.eventId === 'produced') { defaultDescription = 'A remote videographer will operate your camera(s) to record and/or live stream your event. Each event includes additional preparation time for the videographer.'; } else if (agg.eventId === 'producedPAYG') { defaultDescription = 'Pay-as-you-go produced event credit. A remote videographer will operate your camera(s) to record and/or live stream your event.'; } else if (agg.eventId === 'freeProducedPromo') { defaultDescription = 'Complimentary produced event credits included as part of promotional offer.'; } else if (agg.eventId === 'audioMixing') { defaultDescription = 'Professional remote audio mixing for live stream events.'; } else if (agg.eventId === 'freeAudioMixingPromo') { defaultDescription = 'Complimentary audio mixing event credits included as part of promotional offer.'; } else if (agg.eventId === 'staticYearly' || agg.eventId === 'staticPPU') { defaultDescription = 'Automated streaming service - system powers on and streams/records at scheduled time.'; } else if (agg.eventId === 'mobileIOS' || agg.eventId === 'mobileKit') { defaultDescription = 'Mobile streaming via iOS app for simple, high-quality live streaming.'; } else { defaultDescription = agg.eventType.label + ' streaming service.'; } const vgLabel = agg.vgLevel && agg.vgLevel !== 'Standard' ? `${agg.vgLevel} ` : ''; const defaultName = `${lengthLabel} ${vgLabel}${agg.eventType.label} Event Credit`; recurringItems.push({ _itemKey: overrideKey, description: override.customName || defaultName, subDescription: override.customDescription || defaultDescription, qty: agg.totalCount, basePrice: avgBasePrice, finalPrice: avgFinalPrice, discountAmount: totalDiscount, unit: '/Unit', isPromo: agg.eventType.isPromo || false, sortOrder: 1 // Events first }); recurringSubtotal += agg.totalBase; recurringDiscountTotal += totalDiscount; }); // === PLATFORM FEE - Consolidated across all locations === let platformFeeQty = 0; let platformFeeTotalBase = 0; let platformFeeTotalFinal = 0; locations.forEach(loc => { if (loc.excludePlatformFee) return; // Skip locations with platform fee removed const basePlatformFee = loc.platformFee || (PRICING_DATA.platformFees?.standard?.price || 1500); const platformFeePrice = loc.platformFeeCustomPrice ?? basePlatformFee; const platformFeeDiscount = loc.platformFeeDiscount || 0; const platformFeeDiscountType = loc.platformFeeDiscountType || 'percent'; const platformFeeFinal = platformFeeDiscountType === 'percent' ? platformFeePrice * (1 - platformFeeDiscount / 100) : Math.max(0, platformFeePrice - platformFeeDiscount); platformFeeQty += 1; platformFeeTotalBase += platformFeePrice; platformFeeTotalFinal += platformFeeFinal; }); if (platformFeeQty > 0) { const avgBasePrice = platformFeeTotalBase / platformFeeQty; const avgFinalPrice = platformFeeTotalFinal / platformFeeQty; const totalDiscount = platformFeeTotalBase - platformFeeTotalFinal; // Check for line item overrides const override = (quote.lineItemOverrides || {})['platform'] || {}; const hasMobileKit = locations.some(loc => { const ed = loc.eventDetails || {}; return (ed.mobileKit && (ed.mobileKit.count || 0) > 0); }); const defaultName = hasMobileKit ? 'Mobile Kit' : 'Platform Fee'; const defaultDescription = hasMobileKit ? 'Double-bonded cell kit to maximize range for live streaming with bluetooth mics.' : 'Includes LiveControl encoder lease, networking kit lease, technical support, unlimited video archiving, a custom web player, and simulcasting.'; recurringItems.push({ _itemKey: 'platform-fee', description: override.customName || defaultName, subDescription: override.customDescription || defaultDescription, qty: platformFeeQty, basePrice: avgBasePrice, finalPrice: avgFinalPrice, discountAmount: totalDiscount, unit: '/Unit', sortOrder: 2 // Platform fee after events }); recurringSubtotal += platformFeeTotalBase; recurringDiscountTotal += totalDiscount; } // === ENCODER UPGRADES - Consolidated === const encoderAggregates = {}; locations.forEach((loc, idx) => { if (loc.selectedEncoder && !loc.encoderExcluded) { const encData = PRICING_DATA.encoders[loc.selectedEncoder]; if (encData) { const isBasic = ['Standard (IP 1-3 cams)'].includes(loc.selectedEncoder) || encData.cost === 0; if (!isBasic && encData.cost > 0) { const baseAddOnFee = encData.cost || (encData.platformFee ? encData.platformFee - (PRICING_DATA.platformFees?.standard?.price || 1500) : 0); const unitPrice = loc.encoderCustomPrice ?? baseAddOnFee; const discount = loc.encoderDiscount || 0; const discountType = loc.encoderDiscountType || 'percent'; const finalPrice = discountType === 'percent' ? unitPrice * (1 - discount / 100) : Math.max(0, unitPrice - discount); const key = loc.selectedEncoder; if (!encoderAggregates[key]) { encoderAggregates[key] = { encoder: loc.selectedEncoder, qty: 0, totalBase: 0, totalFinal: 0 }; } encoderAggregates[key].qty += 1; encoderAggregates[key].totalBase += unitPrice; encoderAggregates[key].totalFinal += finalPrice; console.log(`📦 Encoder upgrade: ${key} - $${finalPrice}`); } } } }); Object.values(encoderAggregates).forEach(agg => { const avgBase = agg.totalBase / agg.qty; const avgFinal = agg.totalFinal / agg.qty; const totalDiscount = agg.totalBase - agg.totalFinal; // Check for line item overrides const override = (quote.lineItemOverrides || {})['encoder'] || {}; // Use specific SKU name with "Encoder Upgrade: " prefix for customer clarity const encData = PRICING_DATA.encoders[agg.encoder]; const defaultName = encData?.skuName ? `Encoder Upgrade: ${encData.skuName}` : (agg.encoder.includes('Upgrade') ? `Encoder Upgrade: ${agg.encoder}` : `Encoder Upgrade: ${agg.encoder}`); const defaultDescription = 'Encoder upgrade to support additional cameras and/or higher resolution.'; recurringItems.push({ _itemKey: 'encoder', description: override.customName || defaultName, subDescription: override.customDescription || defaultDescription, qty: agg.qty, basePrice: avgBase, finalPrice: avgFinal, discountAmount: totalDiscount, unit: '/Unit', sortOrder: 3 // Encoder upgrades after platform fee }); recurringSubtotal += agg.totalBase; recurringDiscountTotal += totalDiscount; }); // === RECURRING ADD-ONS - Consolidated with discount and qty support === const addonAggregates = {}; locations.forEach(loc => { const addOnOverrides = loc.addOnOverrides || {}; (loc.selectedAddOns || []).forEach(addOnName => { const addOnData = PRICING_DATA.addOns[addOnName]; // New SKU structure: recurring = true, price = annual amount if (!addOnData || !addOnData.recurring) return; // Get override pricing if available const override = addOnOverrides[addOnName] || {}; const qty = override.qty || 1; const basePrice = addOnData.price || 0; const customPrice = override.customAnnual != null ? override.customAnnual : basePrice; const discount = override.discount || 0; const discountType = override.discountType || 'percent'; // Calculate effective price after discount const effectivePrice = discountType === 'percent' ? customPrice * (1 - discount / 100) : Math.max(0, customPrice - discount); const key = addOnName; if (!addonAggregates[key]) { addonAggregates[key] = { name: addOnData.name || addOnName, description: addOnData.description || '', basePrice: customPrice, finalPrice: effectivePrice, discountAmount: customPrice - effectivePrice, totalQty: 0 }; } addonAggregates[key].totalQty += qty; }); }); Object.values(addonAggregates).forEach(agg => { // Check for line item overrides const overrideKey = `addon-${agg.name}`; const override = (quote.lineItemOverrides || {})[overrideKey] || {}; recurringItems.push({ _itemKey: `addon-rec-${agg.name}`, description: override.customName || agg.name, subDescription: override.customDescription || agg.description, qty: agg.totalQty, basePrice: agg.basePrice, finalPrice: agg.finalPrice, discountAmount: agg.discountAmount * agg.totalQty, unit: '/Unit', sortOrder: 4 // Add-ons after encoder }); recurringSubtotal += agg.basePrice * agg.totalQty; recurringDiscountTotal += agg.discountAmount * agg.totalQty; }); // === CAMERAS - Aggregated by model (LAST in recurring) === const cameraAggregates = {}; const cameraPurchaseAggregates = {}; locations.forEach(loc => { const locPayType = loc.paymentType || payType; // Per-location override const lcCameras = loc.lcCameras || []; lcCameras.forEach(cam => { const camData = PRICING_DATA.cameras[cam.model] || PRICING_DATA.cameras['Standard 20x (LC 4K Series 3)']; const basePrice = locPayType === 'lease' ? (camData?.leaseList || camData?.price || 1200) : (camData?.purchaseList || 2300); // Always read from typed pricing objects, fall back to flat fields for backward compat const pricing = (locPayType === 'lease' ? cam.leasePricing : cam.purchasePricing) || {}; const unitPrice = pricing.customPrice != null ? pricing.customPrice : (cam.customPrice != null ? cam.customPrice : basePrice); const discount = pricing.discount != null ? pricing.discount : (cam.discount || 0); const discountType = pricing.discountType || cam.discountType || 'percent'; const finalUnitPrice = discountType === 'percent' ? unitPrice * (1 - discount / 100) : Math.max(0, unitPrice - discount); const aggs = locPayType === 'lease' ? cameraAggregates : cameraPurchaseAggregates; if (!aggs[cam.model]) { aggs[cam.model] = { model: cam.model, qty: 0, totalBase: 0, totalFinal: 0 }; } aggs[cam.model].qty += cam.qty; aggs[cam.model].totalBase += unitPrice * cam.qty; aggs[cam.model].totalFinal += finalUnitPrice * cam.qty; }); }); // Recurring camera leases (LAST) Object.values(cameraAggregates).forEach(agg => { const avgBase = agg.totalBase / agg.qty; const avgFinal = agg.totalFinal / agg.qty; const totalDiscount = agg.totalBase - agg.totalFinal; // Check for line item overrides const overrideKey = `camera-${agg.model}`; const override = (quote.lineItemOverrides || {})[overrideKey] || {}; // Get SKU name if available const camData = PRICING_DATA.cameraLeases?.[agg.model]; const defaultName = camData?.name || `Camera Lease - ${agg.model}`; const defaultDescription = camData?.description || 'Annual camera lease includes hardware warranty and support.'; recurringItems.push({ _itemKey: `cam-lease-${agg.model}`, description: override.customName || defaultName, subDescription: override.customDescription || defaultDescription, qty: agg.qty, basePrice: avgBase, finalPrice: avgFinal, discountAmount: totalDiscount, unit: '/Unit', sortOrder: 5, // Cameras last in recurring _isCameraItem: true }); recurringSubtotal += agg.totalBase; recurringDiscountTotal += totalDiscount; }); // One-time camera purchases Object.values(cameraPurchaseAggregates).forEach(agg => { const avgBase = agg.totalBase / agg.qty; const avgFinal = agg.totalFinal / agg.qty; const totalDiscount = agg.totalBase - agg.totalFinal; // Check for line item overrides const overrideKey = `camera-${agg.model}`; const override = (quote.lineItemOverrides || {})[overrideKey] || {}; // Get SKU name if available const camData = PRICING_DATA.cameraPurchases?.[agg.model]; const defaultName = camData?.name || `Camera Purchase - ${agg.model}`; const defaultDescription = camData?.description || 'One-time camera purchase.'; oneTimeItems.push({ _itemKey: `cam-purchase-${agg.model}`, description: override.customName || defaultName, subDescription: override.customDescription || defaultDescription, qty: agg.qty, basePrice: avgBase, finalPrice: avgFinal, discountAmount: totalDiscount, unit: '', _isCameraItem: true }); oneTimeSubtotal += agg.totalBase; oneTimeDiscountTotal += totalDiscount; }); // === INSTALLATION - Aggregated by type (separate line items per install type) === const installAggregates = {}; locations.forEach((loc, locIdx) => { if (loc.installationRequired && loc.installationType !== 'none') { const instType = loc.installationType || 'standard'; const lcCameras = loc.lcCameras || []; const cameraCount = loc.installationCameraCount ?? lcCameras.reduce((s, c) => s + c.qty, 0); let baseInstallPrice = 0; if (instType === 'custom') { baseInstallPrice = loc.customInstallation?.price || 0; } else if (instType === 'implementationOnly') { baseInstallPrice = PRICING_DATA.installation?.implementation?.price || 500; } else if (instType === 'standard') { const cappedCount = Math.min(Math.max(cameraCount, 1), 6); const instData = PRICING_DATA.installation?.byCameraCount?.[cappedCount]; baseInstallPrice = instData?.price || (cameraCount <= 2 ? 1500 : 1500 + (cameraCount - 2) * 500); } else if (instType === 'noCameras') { baseInstallPrice = PRICING_DATA.installation?.implementation?.price || 500; } else if (instType === 'audioOnly') { baseInstallPrice = PRICING_DATA.installation?.implementationAudio?.price || 500; } const instCustomPrice = loc.installationCustomPrice ?? baseInstallPrice; const instDiscount = loc.installationDiscount || 0; const instDiscountType = loc.installationDiscountType || 'percent'; const instFinal = instDiscountType === 'percent' ? instCustomPrice * (1 - instDiscount / 100) : Math.max(0, instCustomPrice - instDiscount); if (instCustomPrice > 0) { const key = `${instType}-loc${locIdx}`; installAggregates[key] = { type: instType, locationName: loc.name || `Location ${locIdx + 1}`, qty: 1, totalBase: instCustomPrice, totalFinal: instFinal, totalCameras: instType === 'standard' ? cameraCount : 0 }; } } }); // Define sort order for install types so standard appears before implementation const installTypeOrder = { standard: 1, noCameras: 2, custom: 3, implementationOnly: 4, audioOnly: 5 }; Object.values(installAggregates).sort((a, b) => (installTypeOrder[a.type] || 99) - (installTypeOrder[b.type] || 99)).forEach(agg => { const avgBase = agg.totalBase / agg.qty; const avgFinal = agg.totalFinal / agg.qty; const totalDiscount = agg.totalBase - agg.totalFinal; // Check for line item overrides (try type-specific key first, fall back to generic) const override = (quote.lineItemOverrides || {})[`installation-${agg.type}`] || (quote.lineItemOverrides || {})['installation'] || {}; // Build description based on installation type let defaultDescription = 'Installation and Implementation'; let defaultSubDesc = 'Professional installation with remote calibration and onboarding.'; if (agg.type === 'implementationOnly') { defaultDescription = 'Implementation Only'; defaultSubDesc = 'LiveControl remote calibration of cameras, equipment, and venue profile creation (no onsite installation provided).'; } else if (agg.type === 'audioOnly') { defaultDescription = 'Audio Only Installation'; defaultSubDesc = 'Professional audio equipment installation and configuration.'; } else if (agg.type === 'custom') { defaultDescription = 'Custom Installation'; defaultSubDesc = 'Custom installation package.'; } else if (agg.totalCameras > 0) { const locLabel = locations.length > 1 && agg.locationName ? ` (${agg.locationName})` : ''; defaultDescription = `Installation and Implementation - ${agg.totalCameras} Camera${agg.totalCameras !== 1 ? 's' : ''}${locLabel}`; } oneTimeItems.push({ _itemKey: 'installation', description: override.customName || defaultDescription, subDescription: override.customDescription || defaultSubDesc, qty: agg.qty, basePrice: avgBase, finalPrice: avgFinal, discountAmount: totalDiscount, unit: '' }); oneTimeSubtotal += agg.totalBase; oneTimeDiscountTotal += totalDiscount; }); // === HARDWARE ITEMS - Capture Card, SDI Converter === // Note: Capture card and SDI converter costs are now included in encoder upgrade SKUs (4001-4027) // These items are no longer shown separately - the encoder upgrade line item covers all hardware costs // Legacy quotes that still have capture card data will show $0 due to 100% discount being default // === ONE-TIME ADD-ONS - Using new SKU structure (recurring: false) === const oneTimeAddonAggregates = {}; locations.forEach(loc => { const addOnOverrides = loc.addOnOverrides || {}; (loc.selectedAddOns || []).forEach(addOnName => { const addOnData = PRICING_DATA.addOns[addOnName]; // New SKU structure: recurring = false means one-time if (!addOnData || addOnData.recurring) return; // Get override pricing if available const override = addOnOverrides[addOnName] || {}; const qty = override.qty || 1; const basePrice = addOnData.price || 0; const customPrice = override.customAnnual != null ? override.customAnnual : basePrice; const discount = override.discount || 0; const discountType = override.discountType || 'percent'; const effectivePrice = discountType === 'percent' ? customPrice * (1 - discount / 100) : Math.max(0, customPrice - discount); const key = addOnName; if (!oneTimeAddonAggregates[key]) { oneTimeAddonAggregates[key] = { name: addOnData.name || addOnName, description: addOnData.description || '', basePrice: customPrice, finalPrice: effectivePrice, discountAmount: customPrice - effectivePrice, totalQty: 0 }; } oneTimeAddonAggregates[key].totalQty += qty; }); }); Object.values(oneTimeAddonAggregates).forEach(agg => { oneTimeItems.push({ _itemKey: `addon-ot-${agg.name}`, description: agg.name, subDescription: agg.description, qty: agg.totalQty, basePrice: agg.basePrice, finalPrice: agg.finalPrice, discountAmount: agg.discountAmount * agg.totalQty, unit: '' }); oneTimeSubtotal += agg.basePrice * agg.totalQty; oneTimeDiscountTotal += agg.discountAmount * agg.totalQty; }); // === CUSTOM PRODUCTS === const customProds = quote.customProducts || []; customProds.forEach((product, _cpIdx) => { if (!product.name || product.price === 0) return; const basePrice = product.price || 0; const qty = product.qty || 1; const discount = product.discount || 0; const discountType = product.discountType || 'percent'; const finalPrice = discountType === 'percent' ? basePrice * (1 - discount / 100) : Math.max(0, basePrice - discount); const totalDiscount = (basePrice - finalPrice) * qty; const isOneTime = product.type === 'one-time'; const lineItem = { description: product.name, subDescription: product.description || '', qty: qty, basePrice: basePrice, finalPrice: finalPrice, discountAmount: totalDiscount, unit: isOneTime ? '' : '/Unit', _itemKey: `custom-${isOneTime ? 'onetime' : 'recurring'}-${_cpIdx}` }; if (isOneTime) { oneTimeItems.push(lineItem); oneTimeSubtotal += basePrice * qty; oneTimeDiscountTotal += totalDiscount; } else { recurringItems.push(lineItem); recurringSubtotal += basePrice * qty; recurringDiscountTotal += totalDiscount; } }); // Handle legacy customOneTimeProducts for backward compatibility const customOneTimeProds = quote.customOneTimeProducts || []; customOneTimeProds.forEach((product, _lopIdx) => { if (!product.name || product.price === 0) return; const basePrice = product.price || 0; const qty = product.qty || 1; const discount = product.discount || 0; const discountType = product.discountType || 'percent'; const finalPrice = discountType === 'percent' ? basePrice * (1 - discount / 100) : Math.max(0, basePrice - discount); const totalDiscount = (basePrice - finalPrice) * qty; oneTimeItems.push({ _itemKey: `legacy-onetime-${_lopIdx}`, description: product.name, subDescription: product.description || '', qty: qty, basePrice: basePrice, finalPrice: finalPrice, discountAmount: totalDiscount, unit: '' }); oneTimeSubtotal += basePrice * qty; oneTimeDiscountTotal += totalDiscount; }); // Add Additional One-Time Fee for Financing (if enabled and amount > 0) const financingOneTimeAmt = quote.financingOneTimeAmount || 0; const financingOneTimeEn = quote.financingOneTimeEnabled !== undefined ? quote.financingOneTimeEnabled : false; const hasFinancing = quote.showMonthlyFinancing || quote.showQuarterlyFinancing; // Note: financingOneTimeAmount is NOT added as a line item // It's displayed in the Summary section as a Setup Deposit // and deducted from the financed total in the payment breakdown // Per-schedule deposit amounts (new in v2.6.4+) // Use ?? instead of || to preserve explicit $0 values (vs undefined for legacy quotes) const monthlyDeposit = quote.monthlyDepositAmount ?? undefined; const quarterlyDeposit = quote.quarterlyDepositAmount ?? undefined; // Apply custom line item order if defined in quote const lineItemOrder = quote.lineItemOrder || {}; const applyOrder = (items, orderArray) => { if (!orderArray || orderArray.length === 0) return items; const sorted = [...items]; sorted.sort((a, b) => { let aIdx = a._itemKey ? orderArray.indexOf(a._itemKey) : -1; let bIdx = b._itemKey ? orderArray.indexOf(b._itemKey) : -1; if (aIdx === -1) aIdx = orderArray.indexOf(a.description); if (bIdx === -1) bIdx = orderArray.indexOf(b.description); if (aIdx === -1 && bIdx === -1) return 0; if (aIdx === -1) return 1; if (bIdx === -1) return -1; return aIdx - bIdx; }); return sorted; }; const orderedRecurring = applyOrder(recurringItems, lineItemOrder.recurring); const orderedOneTime = applyOrder(oneTimeItems, lineItemOrder.oneTime); return { recurringItems: orderedRecurring, oneTimeItems: orderedOneTime, recurringSubtotal, recurringDiscountTotal, recurringTotal: recurringSubtotal - recurringDiscountTotal, oneTimeSubtotal, oneTimeDiscountTotal, oneTimeTotal: oneTimeSubtotal - oneTimeDiscountTotal, financingOneTimeAmount: (hasFinancing && financingOneTimeEn) ? financingOneTimeAmt : 0, monthlyDepositAmount: monthlyDeposit, quarterlyDepositAmount: quarterlyDeposit }; };