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 (
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
Laporan Kalkulasi {activeTab === 'hydraulic_route' ? 'Hidrolik Sekuensial' : 'Beban Pipa Schedule 40'}
| 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" /> | {/* Auto Calculated Fields */}{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)} |
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.
| 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)} |