WO ID: ${data.id} | Date: ${new Date().toLocaleDateString()}
`;
// 2. Client & Tech Info Table
html += `
I. Job & Technician Data
`;
html += `
| Client: | ${data.client} |
| Address: | ${data.address} |
| Service Rep: | ${data.rep} |
`;
// 3. Work Description
html += `
II. Work Performed
`;
html += `
${data.desc}
`;
// 4. Materials Table
html += `
III. Parts & Labor Breakdown
`;
// Materials Table
let matTable = `
| Item / Part # |
Qty |
Unit Price |
Line Total |
`;
data.materials.forEach(m => {
matTable += `
| ${m.item} |
${m.qty} |
${formatted(m.price)} |
${formatted(m.total)} |
`;
});
matTable += `| MATERIALS SUB-TOTAL | ${formatted(materialsData.totalCost)} |
`;
matTable += `
`;
html += matTable;
// Labor and Totals
html += `
| Labor Hours: | ${data.hours} @ ${formatted(data.rate)} |
| Labor Cost: | ${formatted(data.financials.labor)} |
| GRAND SUBTOTAL: | ${formatted(data.financials.subtotal)} |
| Sales Tax (8.0%): | ${formatted(data.financials.tax)} |
| TOTAL DUE: | ${formatted(data.financials.total)} |
`;
// 5. Observations
html += `
IV. Technician Observations
`;
html += `
${data.obs}
`;
// 6. Signatures
html += `
Technician Signature: ______________________
Client Approval Signature: ______________________
`;
container.innerHTML = html;
}
function wofgSwitchTab(tabId) {
document.querySelectorAll('.wofg-tab-btn').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.wofg-content').forEach(c => c.classList.remove('active'));
const idx = tabId === 'builder' ? 0 : 1;
document.querySelectorAll('.wofg-tab-btn')[idx].classList.add('active');
document.getElementById('wofg-' + tabId).classList.add('active');
if (tabId === 'preview') {
wofgRenderReport();
}
}
function wofgLoadExample() {
if(!confirm("Overwrite current data with example HVAC repair job?")) return;
document.getElementById('inp-id').value = "WO-2025-0101";
document.getElementById('inp-client').value = "MegaTech Office Park";
document.getElementById('inp-address').value = "123 Industrial Way, Chicago, IL 60601";
document.getElementById('inp-rep').value = "A. Martinez";
document.getElementById('inp-rate').value = "85.00";
document.getElementById('inp-hours').value = "3.5";
document.getElementById('inp-desc').value = "Diagnosed motor failure on HVAC unit 4 (Rooftop). Disassembled unit, replaced main blower motor and drive belt (belt was cracked). Reassembled, tested pressures, and verified unit cycling correctly.";
document.getElementById('inp-obs').value = "Unit running efficiently (EER 10.5). Recommend filter replacement next month. Client signed off on completed work at 16:30.";
// Clear and fill materials
document.getElementById('wofg-materials-rows').innerHTML = '';
wofgAddMaterialRow("Blower Motor (Model #XJ-100)", 1, 350.00);
wofgAddMaterialRow("Drive Belt (3/4 x 52 inch)", 1, 18.50);
wofgAddMaterialRow("Capacitor, 40/5 uF", 1, 22.00);
wofgAddMaterialRow("Fuses (10 Amp)", 2, 1.50);
wofgRenderReport();
wofgSwitchTab('preview');
}
/* --- PDF Generation --- */
async function wofgGeneratePDF() {
wofgRenderReport(); // Final render check
const materialsData = wofgGetMaterialsData();
const data = {
id: document.getElementById('inp-id').value || "WO-0000",
client: document.getElementById('inp-client').value || "N/A Client",
address: document.getElementById('inp-address').value || "N/A Address",
rep: document.getElementById('inp-rep').value || "N/A Rep",
rate: parseFloat(document.getElementById('inp-rate').value) || 0,
hours: parseFloat(document.getElementById('inp-hours').value) || 0,
desc: document.getElementById('inp-desc').value || "No job description provided.",
obs: document.getElementById('inp-obs').value || "No observations recorded.",
financials: { labor: 0, subtotal: 0, tax: 0, total: 0 } // Recalculated for PDF
};
const laborCost = data.hours * data.rate;
const subtotal = laborCost + materialsData.totalCost;
const taxRate = 0.08;
const tax = subtotal * taxRate;
const grandTotal = subtotal + tax;
data.financials = { labor: laborCost, subtotal: subtotal, tax: tax, total: grandTotal };
const { jsPDF } = window.jspdf;
const doc = new jsPDF('p', 'mm', 'a4');
const orange = [230, 126, 34];
const slate = [44, 62, 80];
let y = 20;
// 1. Header (WO ID & Date)
doc.setFillColor(...slate);
doc.rect(0, 0, 210, 20, 'F');
doc.setTextColor(255, 255, 255);
doc.setFontSize(16);
doc.text("WORK ORDER / JOB REPORT", 14, 13);
doc.setFontSize(10);
doc.text(`WO ID: ${data.id}`, 14, 17);
doc.text(`Date: ${new Date().toLocaleDateString()}`, 150, 17);
// 2. Job Metadata
doc.setTextColor(0, 0, 0);
doc.setFontSize(10);
doc.setFont("helvetica", "bold");
doc.text("CLIENT:", 14, y + 10);
doc.text("REPRESENTATIVE:", 105, y + 10);
y += 15;
doc.setFont("helvetica", "normal");
doc.text(data.client, 14, y);
doc.text(data.rep, 105, y);
y += 5;
doc.text(data.address, 14, y);
y += 10;
// 3. Work Performed
doc.setFont("helvetica", "bold");
doc.setTextColor(...slate);
doc.setFontSize(12);
doc.text("WORK PERFORMED / DESCRIPTION", 14, y);
y += 5;
doc.setFont("times", "normal");
doc.setFontSize(10);
doc.setTextColor(50, 50, 50);
const splitDesc = doc.splitTextToSize(data.desc, 180);
doc.text(splitDesc, 14, y + 5);
y += (splitDesc.length * 5) + 10;
// 4. Financial Table
doc.setFont("helvetica", "bold");
doc.setTextColor(...slate);
doc.setFontSize(12);
doc.text("FINANCIAL BREAKDOWN", 14, y);
y += 5;
// Materials Table
const matTableBody = materialsData.materials.map(m => [
m.item,
m.qty,
`$${m.price}`,
`$${m.total}`
]);
doc.autoTable({
startY: y,
head: [['Item / Part #', 'Qty', 'Unit Price', 'Line Total']],
body: matTableBody,
theme: 'grid',
headStyles: { fillColor: orange, textColor: 255, fontSize: 9 },
styles: { fontSize: 8.5 },
columnStyles: { 2: { halign: 'right' }, 3: { halign: 'right', fontStyle: 'bold' } }
});
y = doc.lastAutoTable.finalY;
// Totals Summary
const totalsTable = [
['Materials Subtotal', `$${materialsData.totalCost.toFixed(2)}`],
['Labor Hours', `${data.hours.toFixed(1)} @ $${data.rate.toFixed(2)}/hr`],
['Labor Cost', `$${data.financials.labor.toFixed(2)}`],
['GRAND SUBTOTAL', `$${data.financials.subtotal.toFixed(2)}`],
['Sales Tax (8.0%)', `$${data.financials.tax.toFixed(2)}`],
[{content: 'TOTAL DUE', styles: { fontStyle: 'bold', fillColor: orange, textColor: 255 }}, {content: `$${data.financials.total.toFixed(2)}`, styles: { fontStyle: 'bold', fillColor: orange, textColor: 255, halign: 'right' }}]
];
doc.autoTable({
startY: y,
body: totalsTable,
theme: 'plain',
styles: { fontSize: 9, cellPadding: 3 },
columnStyles: { 0: { cellWidth: 35, halign: 'right' }, 1: { halign: 'right', cellWidth: 40 } },
margin: { left: 120 }
});
y = doc.lastAutoTable.finalY + 15;
// 5. Observations
doc.setFont("helvetica", "bold");
doc.text("TECHNICIAN OBSERVATIONS / NOTES", 14, y);
y += 5;
doc.setFont("times", "normal");
doc.setFontSize(10);
doc.text(doc.splitTextToSize(data.obs, 180), 14, y + 5);
y += 20;
// 6. Signatures
doc.setFontSize(10);
doc.text("Technician Signature: ______________________", 14, y + 10);
doc.text("Client Approval Signature: ______________________", 110, y + 10);
doc.save(`WorkOrder_${data.id}.pdf`);
}