-
-
-
-
+
+
+
+
Freight Class
+
--
+
Density: 0.00 lb/ft³
+
+
+
Class 500
Low densityClass 50
High density
-
-
+ e.target.value = '';
+ };
+ reader.readAsText(file);
+}
+
+// ─── Unit toggles ─────────────────────────────────────────────────────────────
+function applyUnitToggleUI() {
+ const du = appState.units.dim;
+ const wu = appState.units.wt;
+ document.querySelectorAll('#dim-unit-toggle button').forEach(b => b.classList.toggle('active', b.dataset.unit === du));
+ document.querySelectorAll('#wt-unit-toggle button').forEach(b => b.classList.toggle('active', b.dataset.unit === wu));
+ document.getElementById('hdr-l').textContent = `L (${du})`;
+ document.getElementById('hdr-w').textContent = `W (${du})`;
+ document.getElementById('hdr-h').textContent = `H (${du})`;
+ document.getElementById('hdr-wt').textContent = `Wt (${wu})`;
+ lucide.createIcons();
+}
+function setDimUnit(newUnit) {
+ const oldUnit = appState.units.dim; if (oldUnit === newUnit) return;
+ const factor = newUnit === 'cm' ? 2.54 : (1/2.54);
+ pieces.forEach(p => {
+ if (p.L) p.L = parseFloat((parseFloat(p.L)*factor).toFixed(2));
+ if (p.W) p.W = parseFloat((parseFloat(p.W)*factor).toFixed(2));
+ if (p.H) p.H = parseFloat((parseFloat(p.H)*factor).toFixed(2));
+ });
+ appState.units.dim = newUnit; saveState();
+ renderPieces(); applyUnitToggleUI();
+}
+function setWtUnit(newUnit) {
+ const oldUnit = appState.units.wt; if (oldUnit === newUnit) return;
+ const factor = newUnit === 'kg' ? (1/2.20462) : 2.20462;
+ pieces.forEach(p => {
+ if (p.Weight) p.Weight = parseFloat((parseFloat(p.Weight)*factor).toFixed(2));
+ });
+ appState.units.wt = newUnit; saveState();
+ renderPieces(); applyUnitToggleUI();
+}
+
+// ─── Pieces ───────────────────────────────────────────────────────────────────
+function renderPieces() {
+ const container = document.getElementById('pieces-container');
+ container.innerHTML = '';
+ pieces.forEach((p, i) => {
+ const row = document.createElement('div'); row.className = 'piece-row';
+ const canDelete = pieces.length > 1;
+ row.innerHTML = `
+
${i+1}
+
+
+
+
+
`;
+ container.appendChild(row);
+ });
+ // Bind input events
+ container.querySelectorAll('input[type="number"]').forEach(el => {
+ el.addEventListener('input', e => { pieces[e.target.dataset.idx][e.target.dataset.field] = e.target.value; });
+ el.addEventListener('keydown', e => { if (e.key === 'Enter') calculateFreightClass(); });
+ });
+ // Bind delete buttons
+ container.querySelectorAll('.piece-delete').forEach(btn => {
+ btn.addEventListener('click', e => {
+ const idx = parseInt(e.currentTarget.dataset.idx);
+ pieces.splice(idx, 1);
+ renderPieces();
+ });
+ });
+ lucide.createIcons();
+}
+function addPiece() {
+ pieces.push({ L:'', W:'', H:'', Weight:'' });
+ renderPieces();
+ // Focus first input of new row
+ const rows = document.querySelectorAll('.piece-row');
+ const lastRow = rows[rows.length-1];
+ if (lastRow) lastRow.querySelector('input') && lastRow.querySelector('input').focus();
+}
+
+// ─── Validation & Clear ───────────────────────────────────────────────────────
+function clearErrors() {
+ document.querySelectorAll('.input-error').forEach(el => el.classList.remove('input-error'));
+ document.getElementById('err-pieces').classList.remove('visible');
+}
+function clearForm() {
+ pieces = [{ L:'', W:'', H:'', Weight:'' }];
+ renderPieces();
+ document.getElementById('class-result').textContent = '--';
+ document.getElementById('density-result').textContent = '0.00';
+ document.getElementById('results-area').classList.remove('active');
+ document.querySelectorAll('tbody tr').forEach(r => r.classList.remove('highlight-row'));
+ document.getElementById('gauge-marker').style.left = '0%';
+ clearErrors();
+}
+
+// ─── Density gauge ────────────────────────────────────────────────────────────
+function updateGauge(density) {
+ // Gauge goes left=0 (low density / high class) to right=50+ (high density / low class)
+ const pct = Math.min(Math.max(density / 50, 0), 1) * 100;
+ document.getElementById('gauge-marker').style.left = pct + '%';
+}
+
+// ─── Calculate ────────────────────────────────────────────────────────────────
+function calculateFreightClass() {
+ clearErrors();
+ const dimFactor = appState.units.dim === 'cm' ? (1/2.54) : 1;
+ const wtFactor = appState.units.wt === 'kg' ? 2.20462 : 1;
+
+ let valid = true;
+ let totalCuFt = 0, totalWt = 0;
+ pieces.forEach((p, i) => {
+ const L = parseFloat(p.L);
+ const W = parseFloat(p.W);
+ const H = parseFloat(p.H);
+ const WT = parseFloat(p.Weight);
+ if (!L || !W || !H || !WT || L<=0 || W<=0 || H<=0 || WT<=0) {
+ valid = false;
+ const rows = document.querySelectorAll('.piece-row');
+ if (rows[i]) rows[i].querySelectorAll('input').forEach(el => {
+ if (!parseFloat(el.value) || parseFloat(el.value)<=0) el.classList.add('input-error');
+ });
+ return; // skip accumulation — prevents NaN from propagating into totals
+ }
+ totalCuFt += (L * dimFactor * W * dimFactor * H * dimFactor) / 1728;
+ totalWt += WT * wtFactor;
+ });
+
+ if (!valid) {
+ document.getElementById('err-pieces').classList.add('visible');
+ return;
+ }
+
+ const density = totalWt / totalCuFt;
+ let cls = 500;
+ for (let r of densityRanges) if (density >= r.min) { cls = r.class; break; }
+
+ // Save to history — store units so restore and display are always correct
+ const histItem = {
+ pieces: pieces.map(p => ({ L:p.L, W:p.W, H:p.H, Weight:p.Weight })),
+ L: pieces[0].L, W: pieces[0].W, H: pieces[0].H, Weight: pieces[0].Weight,
+ Density: density.toFixed(2),
+ Class: cls,
+ timestamp: new Date().toISOString(),
+ units: { dim: appState.units.dim, wt: appState.units.wt }
+ };
+ appState.history.unshift(histItem);
+ if (appState.history.length > 50) appState.history.pop();
+ saveState(); renderHistory();
+
+ document.getElementById('density-result').textContent = density.toFixed(2);
+ document.getElementById('class-result').textContent = cls;
+ document.getElementById('results-area').classList.add('active');
+ document.querySelectorAll('tbody tr').forEach(r => r.classList.remove('highlight-row'));
+ const refRow = document.getElementById(`row-${cls}`);
+ if (refRow) refRow.classList.add('highlight-row');
+ updateGauge(density);
+ lucide.createIcons();
+}
+
+// ─── Copy to clipboard ────────────────────────────────────────────────────────
+function copyResult() {
+ if (document.getElementById('class-result').textContent === '--') return;
+ const cls = document.getElementById('class-result').textContent;
+ const density = document.getElementById('density-result').textContent;
+ const du = appState.units.dim, wu = appState.units.wt;
+ const pLines = pieces.map((p,i)=>` Piece ${i+1}: ${p.L}×${p.W}×${p.H} ${du}, ${p.Weight} ${wu}`).join('\n');
+ const text = `Freight Class: ${cls}\nDensity: ${density} lb/ft³\n${pLines}`;
+ navigator.clipboard.writeText(text).then(() => {
+ const btn = document.getElementById('copy-btn');
+ btn.classList.add('copied');
+ btn.innerHTML = '
Copied!'; lucide.createIcons();
+ setTimeout(() => { btn.classList.remove('copied'); btn.innerHTML = '
Copy'; lucide.createIcons(); }, 2000);
+ });
+}
+
+// ─── PDF Export ───────────────────────────────────────────────────────────────
+function exportPDF() {
+ if (document.getElementById('class-result').textContent === '--') {
+ alert('Please calculate a freight class before exporting a PDF.');
+ return;
+ }
+ if (typeof window.jspdf === 'undefined') { alert('PDF library not loaded. Please check your internet connection.'); return; }
+ const { jsPDF } = window.jspdf;
+ const doc = new jsPDF({ unit:'mm', format:'letter' });
+ const W = doc.internal.pageSize.getWidth();
+ const brandColor = hexToRgb(appState.branding.color || '#2563eb');
+ const cls = document.getElementById('class-result').textContent;
+ const density = document.getElementById('density-result').textContent;
+ const du = appState.units.dim, wu = appState.units.wt;
+ const now = new Date().toLocaleString();
+ let y = 18;
+
+ // Header bar
+ doc.setFillColor(brandColor.r, brandColor.g, brandColor.b);
+ doc.rect(0, 0, W, 28, 'F');
+
+ // Logo — detect format from data URL so JPG/PNG/WEBP all work
+ if (appState.branding.logo) {
+ try { doc.addImage(appState.branding.logo, getImageFormat(appState.branding.logo), 10, 4, 0, 20); } catch(e) {}
+ }
+
+ // Company name
+ doc.setTextColor(255, 255, 255);
+ doc.setFontSize(16); doc.setFont('helvetica','bold');
+ const companyName = appState.branding.name || 'Freight Calculator Pro';
+ doc.text(companyName, W/2, 13, { align:'center' });
+ doc.setFontSize(9); doc.setFont('helvetica','normal');
+ doc.text('Freight Quote', W/2, 21, { align:'center' });
+
+ y = 40;
+ // Date
+ doc.setTextColor(100, 116, 139); doc.setFontSize(8);
+ doc.text(`Generated: ${now}`, W - 14, y - 6, { align:'right' });
+
+ // Class result box
+ doc.setDrawColor(brandColor.r, brandColor.g, brandColor.b);
+ doc.setFillColor(240, 245, 255);
+ doc.roundedRect(14, y, W - 28, 28, 3, 3, 'FD');
+ doc.setTextColor(brandColor.r, brandColor.g, brandColor.b);
+ doc.setFontSize(32); doc.setFont('helvetica','bold');
+ doc.text(`Class ${cls}`, W/2, y + 18, { align:'center' });
+ doc.setFontSize(10); doc.setFont('helvetica','normal');
+ doc.setTextColor(100, 116, 139);
+ doc.text(`Density: ${density} lb/ft³`, W/2, y + 25, { align:'center' });
+
+ y += 38;
+ // Pieces table
+ doc.setFontSize(11); doc.setFont('helvetica','bold'); doc.setTextColor(30, 41, 59);
+ doc.text('Shipment Pieces', 14, y); y += 5;
+
+ const dimFactor = du === 'cm' ? (1/2.54) : 1;
+ const wtFactor = wu === 'kg' ? 2.20462 : 1;
+ const tableBody = pieces.map((p, i) => {
+ const Lin = parseFloat(p.L)*dimFactor, Win = parseFloat(p.W)*dimFactor, Hin = parseFloat(p.H)*dimFactor;
+ const WTlb = parseFloat(p.Weight)*wtFactor;
+ const cuFt = (Lin*Win*Hin)/1728;
+ const pcf = WTlb / cuFt;
+ return [
+ i+1,
+ `${p.L} ${du}`,
+ `${p.W} ${du}`,
+ `${p.H} ${du}`,
+ `${p.Weight} ${wu}`,
+ cuFt.toFixed(3),
+ pcf.toFixed(2)
+ ];
+ });
+ const totalWtLb = pieces.reduce((s,p)=>s+parseFloat(p.Weight||0)*(wu==='kg'?2.20462:1),0);
+ const totalCuFt = pieces.reduce((p,pc)=>{
+ const L=parseFloat(pc.L)*dimFactor, Ww=parseFloat(pc.W)*dimFactor, H=parseFloat(pc.H)*dimFactor;
+ return p+(L*Ww*H)/1728;
+ },0);
+
+ doc.autoTable({
+ startY: y,
+ head: [['#','Length','Width','Height','Weight','Cu Ft','PCF']],
+ body: tableBody,
+ foot: [['','','','',`Total: ${totalWtLb.toFixed(1)} lbs`, `${totalCuFt.toFixed(3)}`, `${(totalWtLb/totalCuFt).toFixed(2)}`]],
+ theme: 'grid',
+ headStyles: { fillColor:[brandColor.r, brandColor.g, brandColor.b], textColor:255, fontStyle:'bold', fontSize:9 },
+ footStyles: { fillColor:[240,245,255], textColor:[30,41,59], fontStyle:'bold', fontSize:9 },
+ bodyStyles: { fontSize:9, textColor:[30,41,59] },
+ alternateRowStyles: { fillColor:[248,250,252] },
+ margin: { left:14, right:14 },
+ columnStyles: { 0:{cellWidth:10, halign:'center'} }
+ });
+
+ // Footer
+ const pageH = doc.internal.pageSize.getHeight();
+ doc.setFontSize(7); doc.setTextColor(148,163,184);
+ doc.text('Generated by Freight Calculator Pro', W/2, pageH-8, { align:'center' });
+
+ doc.save(`freight-class-${cls}-${new Date().toISOString().slice(0,10)}.pdf`);
+}
+
+function getImageFormat(dataUrl) {
+ if (/^data:image\/jpe?g/i.test(dataUrl)) return 'JPEG';
+ if (/^data:image\/png/i.test(dataUrl)) return 'PNG';
+ if (/^data:image\/gif/i.test(dataUrl)) return 'GIF';
+ if (/^data:image\/webp/i.test(dataUrl)) return 'WEBP';
+ return 'PNG';
+}
+
+function hexToRgb(hex) {
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
+ return result ? { r:parseInt(result[1],16), g:parseInt(result[2],16), b:parseInt(result[3],16) } : { r:37, g:99, b:235 };
+}
+
+// ─── NMFC Search ──────────────────────────────────────────────────────────────
+function nmfcSearch(query) {
+ const container = document.getElementById('nmfc-results');
+ const q = query.trim().toLowerCase();
+ if (!q) {
+ container.innerHTML = '
Type to search commodities
';
+ return;
+ }
+ const results = nmfcData.filter(n =>
+ n.desc.toLowerCase().includes(q) || n.code.includes(q)
+ ).slice(0, 20);
+
+ if (!results.length) {
+ container.innerHTML = '
No matching commodities found
';
+ return;
+ }
+ let html = '
| NMFC Code | Description | Std Class |
';
+ results.forEach(n => {
+ html += `
+ | ${n.code} |
+ ${n.desc} |
+ ${n.cls} |
+
`;
+ });
+ html += '
';
+ container.innerHTML = html;
+}
+function nmfcSelect(cls) {
+ // Highlight the matching row in the density reference table
+ document.querySelectorAll('tbody tr').forEach(r => r.classList.remove('highlight-row'));
+ const row = document.getElementById(`row-${cls}`);
+ if (row) {
+ row.classList.add('highlight-row');
+ row.scrollIntoView({ behavior:'smooth', block:'center' });
+ }
+}
+