Spaces:
Running
Running
| import React, { useState, useMemo } from 'react'; | |
| import type { ProcessNode } from '../../types/process'; | |
| import { extractStreamInfo } from '../../utils/streamInfo'; | |
| import { exportProjectToCsv } from '../../utils/csvExport'; | |
| interface Props { | |
| processes: ProcessNode[]; | |
| groups: number[][]; | |
| groupNames: string[]; | |
| groupCoordinates?: Record<string, { lat?: any; lon?: any; hours?: any }>; | |
| } | |
| type SortKey = 'group' | 'subprocess' | 'streamName' | 'type' | 'tin' | 'tout' | 'mdot' | 'cp' | 'CP' | 'Q' | 'lat' | 'lon' | 'hours' | 'water_in' | 'water_out' | 'density' | 'pressure' | 'notes'; | |
| export default function StreamDataTable({ | |
| processes, | |
| groups = [], | |
| groupNames = [], | |
| groupCoordinates = {}, | |
| }: Props) { | |
| const [sortKey, setSortKey] = useState<SortKey>('Q'); | |
| const [sortDir, setSortDir] = useState<'desc' | 'asc'>('desc'); | |
| // Flatten streams | |
| const allRows = useMemo(() => { | |
| const res: any[] = []; | |
| groups.forEach((subIdxs, gIdx) => { | |
| const gName = groupNames[gIdx] || `Process ${gIdx + 1}`; | |
| const gCoord = groupCoordinates[gIdx] || {}; | |
| subIdxs.forEach((si) => { | |
| const sub = processes[si]; | |
| if (!sub) return; | |
| const baseInfo = { | |
| group: gName, | |
| subprocess: sub.name, | |
| lat: sub.lat || gCoord.lat || '', | |
| lon: sub.lon || gCoord.lon || '', | |
| hours: sub.hours || gCoord.hours || '', | |
| water_in: sub.extra_info?.water_content_in || '', | |
| water_out: sub.extra_info?.water_content_out || '', | |
| density: sub.extra_info?.density || '', | |
| pressure: sub.extra_info?.pressure || '', | |
| notes: sub.extra_info?.notes || '', | |
| }; | |
| if (!(sub.streams || []).length && !(sub.children || []).some(c => (c.streams || []).length)) { | |
| res.push({ ...baseInfo, streamName: '', type: '', tin: null, tout: null, mdot: null, cp: null, CP: null, Q: null }); | |
| } | |
| (sub.streams || []).forEach((s) => { | |
| const si = extractStreamInfo(s as any); | |
| res.push({ | |
| ...baseInfo, | |
| ...si, | |
| streamName: s.name, | |
| // Prioritize stream-specific metadata if extracted | |
| water_in: si.water_in || baseInfo.water_in, | |
| water_out: si.water_out || baseInfo.water_out, | |
| density: si.density || baseInfo.density, | |
| pressure: si.pressure || baseInfo.pressure, | |
| }); | |
| }); | |
| (sub.children || []).forEach((child) => { | |
| const childBase = { | |
| group: gName, | |
| subprocess: `${sub.name} › ${child.name}`, | |
| lat: child.lat || sub.lat || gCoord.lat || '', | |
| lon: child.lon || sub.lon || gCoord.lon || '', | |
| hours: child.hours || sub.hours || gCoord.hours || '', | |
| water_in: child.extra_info?.water_content_in || sub.extra_info?.water_content_in || '', | |
| water_out: child.extra_info?.water_content_out || sub.extra_info?.water_content_out || '', | |
| density: child.extra_info?.density || sub.extra_info?.density || '', | |
| pressure: child.extra_info?.pressure || sub.extra_info?.pressure || '', | |
| notes: child.extra_info?.notes || sub.extra_info?.notes || '', | |
| }; | |
| (child.streams || []).forEach((s) => { | |
| const si = extractStreamInfo(s as any); | |
| res.push({ | |
| ...childBase, | |
| ...si, | |
| streamName: s.name, | |
| water_in: si.water_in || childBase.water_in, | |
| water_out: si.water_out || childBase.water_out, | |
| density: si.density || childBase.density, | |
| pressure: si.pressure || childBase.pressure, | |
| }); | |
| }); | |
| }); | |
| }); | |
| }); | |
| return res; | |
| }, [processes, groups, groupNames]); | |
| const sortedRows = useMemo(() => { | |
| return [...allRows].sort((a, b) => { | |
| const vA = a[sortKey]; | |
| const vB = b[sortKey]; | |
| if (vA === vB) return 0; | |
| if (vA == null) return 1; | |
| if (vB == null) return -1; | |
| let res = (vA < vB ? -1 : 1); | |
| return sortDir === 'asc' ? res : -res; | |
| }); | |
| }, [allRows, sortKey, sortDir]); | |
| const toggleSort = (key: SortKey) => { | |
| if (sortKey === key) setSortDir(sortDir === 'asc' ? 'desc' : 'asc'); | |
| else { setSortKey(key); setSortDir('desc'); } | |
| }; | |
| const cellStyle: React.CSSProperties = { | |
| padding: '4px 8px', | |
| fontSize: '11.5px', | |
| verticalAlign: 'middle', | |
| color: 'var(--text-main)', | |
| lineHeight: '1.2', | |
| borderBottom: '1px solid var(--border)', | |
| whiteSpace: 'nowrap', | |
| overflow: 'hidden', | |
| textOverflow: 'ellipsis', | |
| maxWidth: '200px' | |
| }; | |
| const headStyle = (key: SortKey): React.CSSProperties => ({ | |
| padding: '6px 8px', | |
| fontSize: '10.5px', | |
| fontWeight: 700, | |
| textTransform: 'uppercase', | |
| textAlign: 'left', | |
| background: 'var(--surface)', | |
| color: sortKey === key ? 'var(--primary)' : 'var(--text-muted)', | |
| cursor: 'pointer', | |
| borderBottom: '2px solid var(--border)', | |
| userSelect: 'none', | |
| whiteSpace: 'nowrap' | |
| }); | |
| return ( | |
| <div className="stream-data-table card mt-md" style={{ padding: 0, display: 'flex', flexDirection: 'column', height: '500px' }}> | |
| <div className="flex justify-between items-center" style={{ padding: '10px 12px 6px', flexShrink: 0 }}> | |
| <h4 style={{ fontSize: '11px', margin: 0, fontWeight: 700, textTransform: 'uppercase', color: 'var(--text-muted)' }}> | |
| Stream Data <span style={{ fontWeight: 400, opacity: 0.7, marginLeft: 8 }}>— Click header to sort</span> | |
| </h4> | |
| <button | |
| className="btn btn-sm btn-primary" | |
| onClick={() => exportProjectToCsv({ | |
| processes, | |
| proc_groups: groups, | |
| proc_group_names: groupNames, | |
| proc_group_coordinates: groupCoordinates as any, | |
| } as any)} | |
| style={{ fontSize: '10px', padding: '2px 8px' }} | |
| > | |
| 📄 Export CSV | |
| </button> | |
| </div> | |
| <div style={{ flex: 1, overflow: 'auto', borderTop: '1px solid var(--border)' }}> | |
| <table style={{ width: '100%', borderCollapse: 'collapse', tableLayout: 'auto' }}> | |
| <thead style={{ position: 'sticky', top: 0, zIndex: 1, boxShadow: '0 1px 2px rgba(0,0,0,0.05)' }}> | |
| <tr> | |
| {[ | |
| {k:'group', l:'Process'}, {k:'subprocess', l:'Subprocess'}, | |
| {k:'streamName', l:'Stream'}, {k:'type', l:'Type'}, | |
| {k:'tin', l:'Tin'}, {k:'tout', l:'Tout'}, {k:'mdot', l:'ṁ'}, {k:'cp', l:'cp'}, {k:'CP', l:'CP'}, {k:'Q', l:'Q (kW)'}, | |
| {k:'lat', l:'Lat'}, {k:'lon', l:'Lon'}, {k:'hours', l:'Hours'}, | |
| {k:'water_in', l:'Water In'}, {k:'water_out', l:'Water Out'}, {k:'density', l:'Density'}, {k:'pressure', l:'Pressure'}, | |
| {k:'notes', l:'Notes'}, | |
| ].map(h => ( | |
| <th key={h.k} style={headStyle(h.k as SortKey)} onClick={() => toggleSort(h.k as SortKey)}> | |
| {h.l} {sortKey === h.k ? (sortDir === 'asc' ? '▴' : '▾') : ''} | |
| </th> | |
| ))} | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {sortedRows.length === 0 ? ( | |
| <tr><td colSpan={18} style={{ padding: 20, textAlign: 'center', color: 'var(--text-muted)' }}>No stream data found</td></tr> | |
| ) : ( | |
| sortedRows.map((r, i) => ( | |
| <tr key={i} style={{ background: i % 2 === 0 ? 'transparent' : 'var(--surface-hover)' }}> | |
| <td style={cellStyle} title={r.group}>{r.group}</td> | |
| <td style={cellStyle} title={r.subprocess}>{r.subprocess}</td> | |
| <td style={cellStyle} title={r.streamName}>{r.streamName}</td> | |
| <td style={cellStyle}>{r.type}</td> | |
| <td style={cellStyle}>{r.tin?.toFixed(1) ?? '—'}</td> | |
| <td style={cellStyle}>{r.tout?.toFixed(1) ?? '—'}</td> | |
| <td style={cellStyle}>{r.mdot?.toFixed(2) ?? '—'}</td> | |
| <td style={cellStyle}>{r.cp?.toFixed(2) ?? '—'}</td> | |
| <td style={cellStyle}>{r.CP?.toFixed(2) ?? '—'}</td> | |
| <td style={{ ...cellStyle, fontWeight: 700, color: 'var(--primary)' }}>{r.Q?.toFixed(1) ?? '—'}</td> | |
| <td style={cellStyle}>{typeof r.lat === 'number' ? r.lat.toFixed(6) : r.lat}</td> | |
| <td style={cellStyle}>{typeof r.lon === 'number' ? r.lon.toFixed(6) : r.lon}</td> | |
| <td style={cellStyle}>{r.hours}</td> | |
| <td style={cellStyle}>{r.water_in}</td> | |
| <td style={cellStyle}>{r.water_out}</td> | |
| <td style={cellStyle}>{r.density}</td> | |
| <td style={cellStyle}>{r.pressure}</td> | |
| <td style={{ ...cellStyle, fontStyle: 'italic', fontSize: '10.5px' }} title={r.notes}>{r.notes}</td> | |
| </tr> | |
| )) | |
| )} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| ); | |
| } | |