import React, { useState, useEffect } from 'react'; import { Calculator, Droplets, ArrowUpRight, Gauge, AlertTriangle, Scale, Plus, Trash2, Layers, TableProperties, Download, Printer } from 'lucide-react'; const App = () => { // --- State Modal Notifikasi --- const [showModal, setShowModal] = useState(false); // --- Tab State --- const [activeTab, setActiveTab] = useState('hydraulic_route'); // 'hydraulic_route' | 'weight' // Fungsi pembuat ID unik yang aman const generateId = () => typeof crypto !== 'undefined' && crypto.randomUUID ? crypto.randomUUID() : Date.now() + Math.random(); // ========================================== // TAB 1: KALKULATOR RUTE HIDROLIK (DARI WORD) // ========================================== const [sourcePressure, setSourcePressure] = useState(11); // bar_g const [roughness, setRoughness] = useState(0.045); // Epsilon mm const [routingRows, setRoutingRows] = useState([ { id: 'p1', name: 'P1', diameter: 202.7, length: 7.8, flow: 1000, minorLoss: 0.052 }, { id: 'p2', name: 'P2', diameter: 202.7, length: 16, flow: 1000, minorLoss: 0.008 }, { id: 'p3', name: 'P3', diameter: 202.7, length: 20, flow: 1000, minorLoss: 0.008 }, { id: 'p4', name: 'P4', diameter: 202.7, length: 30, flow: 1000, minorLoss: 0.008 }, { id: 'p5', name: 'P5', diameter: 202.7, length: 31, flow: 1000, minorLoss: 0.017 }, ]); const addRoutingRow = () => { const lastRow = routingRows[routingRows.length - 1]; setRoutingRows([...routingRows, { id: generateId(), name: `P${routingRows.length + 1}`, diameter: lastRow ? lastRow.diameter : 202.7, length: 10, flow: lastRow ? lastRow.flow : 1000, minorLoss: 0 }]); }; const removeRoutingRow = (id) => setRoutingRows(routingRows.filter(row => row.id !== id)); const updateRoutingRow = (id, field, value) => { setRoutingRows(routingRows.map(row => row.id === id ? { ...row, [field]: value } : row)); }; // Kalkulasi Sekuensial let currentEntryPressure = parseFloat(sourcePressure) || 0; const computedRoutingRows = routingRows.map((row) => { const Q_usgpm = parseFloat(row.flow) || 0; const Q_m3s = Q_usgpm * 0.0000630902; // Konversi USGPM ke m³/s const massFlow = Q_m3s * 1000; // kg/s (asumsi densitas air 1000 kg/m³) const D_m = (parseFloat(row.diameter) || 0) / 1000; const L = parseFloat(row.length) || 0; const minorLoss = parseFloat(row.minorLoss) || 0; const eps_m = parseFloat(roughness) / 1000; let velocity = 0; let frictionLossBar = 0; let totalLossBar = 0; let exitPressure = currentEntryPressure; if (D_m > 0 && Q_m3s > 0) { const area = (Math.PI / 4) * Math.pow(D_m, 2); velocity = Q_m3s / area; // Darcy-Weisbach & Swamee-Jain const Re = (velocity * D_m) / 1.004e-6; // Viskositas kinematik air ~20°C let f = 0; if (Re > 0) { if (Re < 2300) { f = 64 / Re; } else { const term1 = eps_m / (3.7 * D_m); const term2 = 5.74 / Math.pow(Re, 0.9); f = 0.25 / Math.pow(Math.log10(term1 + term2), 2); } const g = 9.81; const frictionLossM = f * (L / D_m) * (Math.pow(velocity, 2) / (2 * g)); frictionLossBar = frictionLossM / 10.197; // Konversi meter head ke Bar } } totalLossBar = frictionLossBar + minorLoss; exitPressure = currentEntryPressure - totalLossBar; const result = { ...row, massFlow, velocity, frictionLossBar, totalLossBar, entryPressure: currentEntryPressure, exitPressure }; currentEntryPressure = exitPressure; // Update untuk baris selanjutnya return result; }); // ========================================== // TAB 2: KALKULATOR BEBAN PIPA (DARI EXCEL) // ========================================== const pipeSpecs = { 25: { name: "DN25 (1\")", od: 33.4, t: 3.38 }, 32: { name: "DN32 (1 1/4\")", od: 42.2, t: 3.56 }, 40: { name: "DN40 (1 1/2\")", od: 48.3, t: 3.68 }, 50: { name: "DN50 (2\")", od: 60.3, t: 3.91 }, 65: { name: "DN65 (2 1/2\")", od: 73.0, t: 5.16 }, 80: { name: "DN80 (3\")", od: 88.9, t: 5.49 }, 100: { name: "DN100 (4\")", od: 114.3, t: 6.02 }, 150: { name: "DN150 (6\")", od: 168.3, t: 7.11 } }; const [distanceBetweenPipes, setDistanceBetweenPipes] = useState(2.46); const [weightRows, setWeightRows] = useState([ { id: 'w1', dn: 25, length: 8 }, { id: 'w2', dn: 32, length: 4 } ]); const addWeightRow = () => setWeightRows([...weightRows, { id: generateId(), dn: 25, length: 1 }]); const removeWeightRow = (id) => setWeightRows(weightRows.filter(row => row.id !== id)); const updateWeightRow = (id, field, value) => setWeightRows(weightRows.map(row => row.id === id ? { ...row, [field]: value } : row)); const renderedWeightRows = weightRows.map(row => { const spec = pipeSpecs[row.dn]; const id_mm = spec.od - (2 * spec.t); const pipeAreaM2 = (Math.PI / 4) * (Math.pow(spec.od / 1000, 2) - Math.pow(id_mm / 1000, 2)); const pipeWeightM = pipeAreaM2 * 7850; const waterAreaM2 = (Math.PI / 4) * Math.pow(id_mm / 1000, 2); const waterWeightM = waterAreaM2 * 1000; const totalWeightM = pipeWeightM + waterWeightM; const totalWeightKg = totalWeightM * (parseFloat(row.length) || 0); const weightPerM2SF = (totalWeightM / parseFloat(distanceBetweenPipes || 1)) * 1.2; return { ...row, spec, id_mm, pipeWeightM, waterWeightM, totalWeightM, totalWeightKg, weightPerM2SF }; }); // Handle PDF Export const handlePrint = () => { try { // Periksa apakah window.print diblokir di iframe if (document.queryCommandSupported && !document.queryCommandSupported('print')) { setShowModal(true); } else { window.print(); } } catch (e) { // Jika terjadi cross-origin iframe error atau sandbox block setShowModal(true); } }; return (
{/* CSS Khusus untuk Mode Cetak / PDF */} {/* MODAL NOTIFIKASI JIKA PRINT DIBLOKIR */} {showModal && (

Pemberitahuan Sistem

Fungsi cetak otomatis diblokir oleh lingkungan preview browser ini demi keamanan.

Untuk menyimpan sebagai PDF, silakan gunakan tombol pintas (shortcut) berikut di keyboard Anda:

Ctrl + P untuk Windows
Cmd ⌘ + P untuk Mac

)}
{/* Header Section */}

Engineer Proteksi Kebakaran

Laporan Kalkulasi {activeTab === 'hydraulic_route' ? 'Hidrolik Sekuensial' : 'Beban Pipa Schedule 40'}

{/* Tombol Cetak PDF */}
{/* Navigation Tabs - Hidden on Print */}
{/* ================= TAB 1: KALKULATOR RUTE HIDROLIK ================= */} {activeTab === 'hydraulic_route' && (

Sprinkler Hydraulic Calculation

{/* Global Settings */}
setSourcePressure(e.target.value)} className="w-24 px-3 py-1.5 bg-white border border-slate-300 rounded focus:ring-2 focus:ring-blue-500 outline-none print:w-auto" />
setRoughness(e.target.value)} className="w-24 px-3 py-1.5 bg-white border border-slate-300 rounded focus:ring-2 focus:ring-blue-500 outline-none print:w-auto" />
{/* Tabel Routing Hidrolik */}
{computedRoutingRows.map((row) => ( {/* Auto Calculated Fields */} ))}
Aksi Pipe Name ID (mm) Length (m) Vol Flow (USGPM) Mass Flow (kg/s) Velocity (m/s) dP MinorLoss (bar) dP TotalLoss (bar) Entry P. (bar_g) Exit P. (bar_g)
updateRoutingRow(row.id, 'name', e.target.value)} className="w-20 px-2 py-1.5 bg-transparent border-b border-dashed border-slate-300 focus:border-blue-500 outline-none print:w-auto" /> updateRoutingRow(row.id, 'diameter', e.target.value)} className="w-20 px-2 py-1.5 bg-white border border-slate-200 rounded focus:ring-1 focus:ring-blue-500 outline-none print:w-auto" /> updateRoutingRow(row.id, 'length', e.target.value)} className="w-20 px-2 py-1.5 bg-white border border-slate-200 rounded focus:ring-1 focus:ring-blue-500 outline-none print:w-auto" /> updateRoutingRow(row.id, 'flow', e.target.value)} className="w-24 px-2 py-1.5 bg-white border border-slate-200 rounded focus:ring-1 focus:ring-blue-500 outline-none text-blue-700 font-medium print:w-auto" /> {row.massFlow.toFixed(3)} {row.velocity.toFixed(3)} updateRoutingRow(row.id, 'minorLoss', e.target.value)} className="w-20 px-2 py-1.5 bg-white border border-slate-200 rounded focus:ring-1 focus:ring-blue-500 outline-none print:w-auto" /> {row.totalLossBar.toFixed(3)} {row.entryPressure.toFixed(5)} {row.exitPressure.toFixed(5)}
Total Pressure Drop: {(currentEntryPressure ? (parseFloat(sourcePressure) - currentEntryPressure) : 0).toFixed(3)} bar

Cara Kerja Node-to-Node: Tabel ini menghitung kehilangan tekanan secara berurutan. Exit Pressure dari baris di atasnya secara otomatis menjadi Entry Pressure untuk baris di bawahnya. Velocity dan Friction Loss dihitung otomatis secara akurat menggunakan formula Darcy-Weisbach.

)} {/* ================= TAB 2: KALKULATOR BEBAN PIPA ================= */} {activeTab === 'weight' && (

Perhitungan Berat Pipa (Schedule 40)

setDistanceBetweenPipes(e.target.value)} className="w-24 px-3 py-1.5 bg-white border border-slate-300 rounded focus:ring-2 focus:ring-blue-500 outline-none print:w-auto" />
{renderedWeightRows.map((row) => ( ))}
Ukuran (DN) OD / t / ID (mm) Berat Pipa + Air (kg/m) Panjang (m) Beban Area + SF 20% (kg/m²) Total Berat (kg) Aksi
{row.spec.od} / {row.spec.t} / {row.id_mm.toFixed(2)} {row.totalWeightM.toFixed(2)}
Pipa: {row.pipeWeightM.toFixed(2)} | Air: {row.waterWeightM.toFixed(2)}
updateWeightRow(row.id, 'length', e.target.value)} className="w-full px-3 py-1.5 bg-white border border-slate-300 rounded focus:ring-2 focus:ring-blue-500 outline-none print:w-auto" /> {row.weightPerM2SF.toFixed(2)} {row.totalWeightKg.toFixed(2)}
)}
); }; export default App;