HeatTransPlan / frontend /src /components /process /SubprocessCard.tsx
drzg15's picture
Initial code commit with LFS for binaries
c993983
import { useState } from 'react';
import type { ProcessNode } from '../../types/process';
import type { Stream } from '../../types/stream';
import StreamEditor from './StreamEditor';
import { useUIStore } from '../../store/uiStore';
import './SubprocessCard.css';
interface Props {
id?: string;
subprocess: ProcessNode;
subIdx: number;
groupIdx: number;
allProcessNames: string[];
onUpdate: (updated: ProcessNode) => void;
onDelete: () => void;
onPlaceRequest: (target: string) => void;
placementTarget: string | null;
}
const DEFAULT_STREAM: Stream = {
name: 'Stream 1',
type: 'product',
display_vars: ['Tin', 'Tout', 'ṁ', 'cp'],
stream_values: {},
};
export default function SubprocessCard({
id,
subprocess,
subIdx,
groupIdx: _groupIdx,
allProcessNames,
onUpdate,
onDelete,
onPlaceRequest,
placementTarget,
}: Props) {
const expanded = useUIStore((s) => s.expandedSubprocesses.has(subIdx));
const setExpanded = (v: boolean) => useUIStore.getState().setSubprocessExpanded(subIdx, v);
const activeSection = useUIStore((s) => s.activeSections[subIdx] || 'streams');
const setActiveSection = (sec: string) => useUIStore.getState().setActiveSection(subIdx, sec);
const [confirmDel, setConfirmDel] = useState(false);
const isPlacing = placementTarget === `sub_${subIdx}`;
const update = (partial: Partial<ProcessNode>) => {
onUpdate({ ...subprocess, ...partial });
};
const addStream = () => {
const streams = [...(subprocess.streams || [])];
streams.push({
...DEFAULT_STREAM,
name: `Stream ${streams.length + 1}`,
});
update({ streams });
};
const updateStream = (sIdx: number, updated: Stream) => {
const streams = [...(subprocess.streams || [])];
streams[sIdx] = updated;
update({ streams });
};
const deleteStream = (sIdx: number) => {
const streams = (subprocess.streams || []).filter((_, i) => i !== sIdx);
update({ streams });
};
// Build next options: all other process names
const nextOptions = allProcessNames.filter((n) => n !== subprocess.name);
const selectedNext = subprocess.next
? subprocess.next.split(',').map((s) => s.trim()).filter(Boolean)
: [];
return (
<div className="sp-card" id={id}>
{/* Header */}
<div className="sp-header">
<button
className="pgl-toggle"
onClick={() => {
setExpanded(!expanded);
}}
>
<span className={`collapsible-arrow ${expanded ? 'open' : ''}`}>
</span>
</button>
<input
type="text"
className="sp-name-input"
value={subprocess.name}
onChange={(e) => update({ name: e.target.value })}
/>
<div className="pgl-scale">
<input
type="range"
min={0.5}
max={3}
step={0.1}
value={subprocess.box_scale || 1.0}
onChange={(e) =>
update({ box_scale: parseFloat(e.target.value) })
}
/>
</div>
<button
className={`btn btn-sm ${isPlacing ? 'btn-primary' : ''}`}
onClick={() =>
onPlaceRequest(isPlacing ? '' : `sub_${subIdx}`)
}
>
{isPlacing ? 'Done' : 'Place'}
</button>
{confirmDel ? (
<span className="pgl-confirm">
<button className="btn btn-sm btn-danger" onClick={onDelete}>
</button>
<button
className="btn btn-sm"
onClick={() => setConfirmDel(false)}
>
</button>
</span>
) : (
<button
className="btn btn-sm"
onClick={() => setConfirmDel(true)}
>
</button>
)}
</div>
{/* Expanded body */}
{expanded && (
<div className="sp-body">
{/* Section tabs */}
<div className="sp-tabs">
{[
{ key: 'relationships', label: 'Relationships', color: '#2980b9' },
{ key: 'streams', label: 'Streams', color: '#e74c3c' },
{ key: 'notes', label: 'Notes', color: '#f39c12' },
].map((tab) => (
<button
key={tab.key}
className={`sp-tab ${activeSection === tab.key ? 'active' : ''}`}
style={
activeSection === tab.key
? { borderBottomColor: tab.color }
: {}
}
onClick={() => setActiveSection(tab.key)}
>
{tab.label}
</button>
))}
</div>
{activeSection === 'relationships' && (
<div className="sp-section" style={{ padding: '8px 0' }}>
<label style={{ fontSize: '11px', color: 'var(--text-muted)' }}>Next Processes (Click to link)</label>
<div className="flex" style={{ flexWrap: 'wrap', gap: '4px', marginTop: '6px' }}>
{nextOptions.length === 0 && <span style={{ fontSize: '11px', color: '#999' }}>No other processes available.</span>}
{nextOptions.map((opt) => {
const isSelected = selectedNext.includes(opt);
return (
<button
key={opt}
className={`btn btn-sm ${isSelected ? 'btn-primary' : ''}`}
style={{ padding: '2px 8px', fontSize: '11px', borderRadius: '12px', border: isSelected ? 'none' : '1px solid var(--border-strong)' }}
onClick={() => {
if (isSelected) {
update({ next: selectedNext.filter((n) => n !== opt).join(', ') });
} else {
update({ next: [...selectedNext, opt].join(', ') });
}
}}
>
{opt} {isSelected && '✓'}
</button>
);
})}
</div>
</div>
)}
{/* Streams */}
{activeSection === 'streams' && (
<div className="sp-section">
<div className="flex justify-between items-center mb-sm">
<span className="sp-section-title">
Streams ({subprocess.streams?.length || 0})
</span>
<button className="btn btn-sm btn-primary" onClick={addStream}>
➕ Add Stream
</button>
</div>
{(subprocess.streams || []).map((stream, sIdx) => (
<StreamEditor
key={sIdx}
id={`stream-editor-${subIdx}-${sIdx}`}
stream={stream}
onChange={(updated) => updateStream(sIdx, updated)}
onDelete={() => deleteStream(sIdx)}
/>
))}
</div>
)}
{/* Parameters & Notes */}
{activeSection === 'notes' && (
<div className="sp-section">
<div className="pgl-info-grid mb-sm">
<div>
<label>Density (kg/m³)</label>
<input
type="text"
value={subprocess.extra_info?.density || ''}
onChange={(e) =>
update({
extra_info: {
...(subprocess.extra_info || {}),
density: e.target.value,
},
})
}
/>
</div>
<div>
<label>Pressure (bar)</label>
<input
type="text"
value={subprocess.extra_info?.pressure || ''}
onChange={(e) =>
update({
extra_info: {
...(subprocess.extra_info || {}),
pressure: e.target.value,
},
})
}
/>
</div>
<div>
<label>Water In (%)</label>
<input
type="text"
value={subprocess.extra_info?.water_content_in || ''}
onChange={(e) =>
update({
extra_info: {
...(subprocess.extra_info || {}),
water_content_in: e.target.value,
},
})
}
/>
</div>
<div>
<label>Water Out (%)</label>
<input
type="text"
value={subprocess.extra_info?.water_content_out || ''}
onChange={(e) =>
update({
extra_info: {
...(subprocess.extra_info || {}),
water_content_out: e.target.value,
},
})
}
/>
</div>
</div>
<label>Notes</label>
<textarea
value={subprocess.extra_info?.notes || ''}
onChange={(e) =>
update({
extra_info: {
...(subprocess.extra_info || {}),
notes: e.target.value,
},
})
}
rows={3}
style={{ width: '100%' }}
placeholder="Add notes about this subprocess..."
/>
</div>
)}
</div>
)}
</div>
);
}