// Megosztott nézetek: Dashboard, Ütemterv (Gantt), Dokumentumok
const Dashboard = ({ user }) => {
const [, force] = React.useReducer(x => x + 1, 0);
const [edit, setEdit] = React.useState(false);
const [openPhaseId, setOpenPhase] = React.useState(null);
const [editTask, setEditTask] = React.useState(null);
const [newTaskFor, setNewTaskFor] = React.useState(null);
const [editPhase, setEditPhase] = React.useState(null);
const [newPhase, setNewPhase] = React.useState(false);
const [confirmDel, setConfirmDel] = React.useState(null);
const totalBudget = window.totalBudget();
const totalSpent = window.totalSpent();
const totalPending= window.totalPending();
const totalPaid = window.totalPaid();
const isAdmin = user?.role === 'admin';
return (
{isAdmin && (
setEdit(!edit)}>
{edit ? 'Kész' : 'Szerkesztés'}
)}
{/* Pénzügyi áttekintés */}
Összköltségvetés
{window.formatHufShort(totalBudget)} Ft
Elköltve
{Math.round(totalSpent / totalBudget * 100)}%
{/* Fázisok bontva */}
Fázisonként
{edit && (
setNewPhase(true)}>
Új fázis
)}
{window.PHASES.map(p => {
const total = window.phaseBudget(p.id);
const spent = window.phaseSpent(p.id);
if (total === 0 && !edit) return null;
const tasks = window.PARENT_TASKS.filter(t => t.phaseId === p.id);
const isOpen = edit && openPhaseId === p.id;
return (
edit && setOpenPhase(isOpen ? null : p.id)}
style={{ flex: 1, minWidth: 0, textAlign: 'left', cursor: edit ? 'pointer' : 'default' }}>
{window.formatHufShort(spent)}
/ {window.formatHufShort(total)}
{edit && (
setEditPhase(p)} style={{ padding: 6 }}>
setConfirmDel({ kind: 'phase', target: p })} style={{ padding: 6 }}>
setOpenPhase(isOpen ? null : p.id)} style={{ padding: 4 }}>
)}
{isOpen && (
{tasks.length === 0 && (
Még nincs tétel ebben a fázisban.
)}
{tasks.map(t => {
const sp = window.parentTaskSpent(t.id);
return (
{window.formatHufShort(t.budget)} Ft
setEditTask({ task: t, phase: p })} style={{ padding: 6 }}>
setConfirmDel({ kind: 'task', target: t })} style={{ padding: 6 }}>
);
})}
setNewTaskFor(p)}> Új tétel
)}
);
})}
{/* Edit sheets — admin */}
{newTaskFor && (
setNewTaskFor(null)} onSave={async (data) => {
try {
await window.api.parentTasks.save({ phase_id: newTaskFor.id, name: data.name, budget: data.budget });
await window.refreshData();
} catch (err) { alert(window.apiErrorMessage(err)); }
setNewTaskFor(null); force();
}}/>
)}
{editTask && (
setEditTask(null)} onSave={async (data) => {
try {
await window.api.parentTasks.save({ id: editTask.task.id, phase_id: editTask.phase.id, name: data.name, budget: data.budget });
await window.refreshData();
} catch (err) { alert(window.apiErrorMessage(err)); }
editTask.task.name = data.name;
editTask.task.budget = data.budget;
setEditTask(null); force();
}}/>
)}
{newPhase && (
setNewPhase(false)} onSave={async (data) => {
try {
await window.api.phases.save(data);
await window.refreshData();
} catch (err) { alert(window.apiErrorMessage(err)); }
setNewPhase(false); force();
}}/>
)}
{editPhase && (
setEditPhase(null)} onSave={async (data) => {
try {
await window.api.phases.save({ id: editPhase.id, ...data });
await window.refreshData();
} catch (err) { alert(window.apiErrorMessage(err)); }
Object.assign(editPhase, data);
setEditPhase(null); force();
}}/>
)}
{confirmDel && (
setConfirmDel(null)} title="Törlés">
Biztosan törölni szeretnéd:
{confirmDel.target.name} ?
{confirmDel.kind === 'phase' && (
A fázishoz tartozó {window.PARENT_TASKS.filter(t => t.phaseId === confirmDel.target.id).length} tétel is törlődik.
)}
setConfirmDel(null)}>Mégse
{
try {
if (confirmDel.kind === 'task') {
await window.api.parentTasks.del(confirmDel.target.id);
} else {
await window.api.phases.del(confirmDel.target.id);
}
await window.refreshData();
} catch (err) { alert(window.apiErrorMessage(err)); }
if (confirmDel.kind === 'task') {
const i = window.PARENT_TASKS.findIndex(x => x.id === confirmDel.target.id);
if (i >= 0) window.PARENT_TASKS.splice(i, 1);
} else {
const phaseId = confirmDel.target.id;
window.PARENT_TASKS = window.PARENT_TASKS.filter(t => t.phaseId !== phaseId);
const i = window.PHASES.findIndex(x => x.id === phaseId);
if (i >= 0) window.PHASES.splice(i, 1);
}
setConfirmDel(null); force();
}}>Törlés
)}
);
};
const Mini = ({ label, value, color = 'var(--ink)' }) => (
{label}
{window.formatHufShort(value)}
);
// === Ütemterv (Gantt mobil-barát) ===
const Timeline = ({ user }) => {
const [, force] = React.useReducer(x => x + 1, 0);
const [edit, setEdit] = React.useState(false);
const [editPhase, setEditPhase] = React.useState(null);
const [newPhase, setNewPhase] = React.useState(false);
const isAdmin = user?.role === 'admin';
const today = new Date(window.TODAY);
const phases = edit ? window.PHASES : window.PHASES.filter(p => window.phaseBudget(p.id) > 0);
if (phases.length === 0) {
return (
Ütemterv
Még nincs fázis.
);
}
// Időablak: legkorábbi start → legkésőbbi end
const dates = phases.flatMap(p => [new Date(p.start), new Date(p.end)]);
const minD = new Date(Math.min(...dates));
const maxD = new Date(Math.max(...dates));
const totalDays = (maxD - minD) / 86400000;
// Hónap-fejléc
const months = [];
let cur = new Date(minD.getFullYear(), minD.getMonth(), 1);
while (cur <= maxD) {
months.push(new Date(cur));
cur = new Date(cur.getFullYear(), cur.getMonth() + 1, 1);
}
const monthLabels = ['jan', 'feb', 'már', 'ápr', 'máj', 'jún', 'júl', 'aug', 'szept', 'okt', 'nov', 'dec'];
const todayPct = ((today - minD) / 86400000) / totalDays * 100;
return (
Ütemterv
{isAdmin && (
setEdit(!edit)}>
{edit ? 'Kész' : 'Szerkesztés'}
)}
{edit
? 'Kattints egy fázisra a kezdő/záró dátum vagy név módosításához.'
: 'Tervezett ütemezés. A piros sávok lemaradásban vannak a mai naphoz képest.'}
{/* Hónap fejléc */}
{months.map((m, i) => {
const pct = ((m - minD) / 86400000) / totalDays * 100;
return (
{monthLabels[m.getMonth()]} {String(m.getFullYear()).slice(2)}
);
})}
{/* Today marker */}
{/* Sávok */}
{phases.map((p, i) => {
const start = new Date(p.start);
const end = new Date(p.end);
const startPct = ((start - minD) / 86400000) / totalDays * 100;
const widthPct = ((end - start) / 86400000) / totalDays * 100;
const overdue = today > end && window.phaseSpent(p.id) < window.phaseBudget(p.id) * 0.95;
const inProgress = today >= start && today <= end;
const done = window.phaseSpent(p.id) >= window.phaseBudget(p.id) * 0.95;
const progressPct = window.phaseBudget(p.id) > 0
? Math.min(100, (window.phaseSpent(p.id) / window.phaseBudget(p.id)) * 100) : 0;
return (
setEditPhase(p) : undefined}
style={{ display: 'flex', alignItems: 'center', padding: '6px 12px', gap: 10, borderBottom: i < phases.length - 1 ? '1px solid var(--line-2)' : 'none', cursor: edit ? 'pointer' : 'default' }}>
{p.code}
{p.name}
{edit && }
{/* Today line */}
{/* Bar */}
);
})}
{/* Legenda */}
kész
folyamatban
lemaradásban
ma ({window.formatDate(window.TODAY)})
{edit && (
setNewPhase(true)} style={{ alignSelf: 'flex-start' }}>
Új fázis
)}
{editPhase && (
setEditPhase(null)} onSave={async (data) => {
try {
await window.api.phases.save({ id: editPhase.id, ...data });
await window.refreshData();
} catch (err) { alert(window.apiErrorMessage(err)); }
Object.assign(editPhase, data);
setEditPhase(null); force();
}}/>
)}
{newPhase && (
setNewPhase(false)} onSave={async (data) => {
try {
await window.api.phases.save(data);
await window.refreshData();
} catch (err) { alert(window.apiErrorMessage(err)); }
setNewPhase(false); force();
}}/>
)}
);
};
// === Dokumentumok ===
const Documents = ({ user }) => {
const [filter, setFilter] = React.useState('all');
const [showUpload, setShowUpload] = React.useState(false);
const tags = ['all', ...Object.keys(window.TAG_COLORS)];
const docs = filter === 'all' ? window.DOCUMENTS : window.DOCUMENTS.filter(d => d.tag === filter);
return (
Dokumentumok
setShowUpload(true)}> Feltölt
{/* Tag szűrő */}
{tags.map(t => {
const color = window.TAG_COLORS[t];
const active = filter === t;
return (
setFilter(t)}
style={{ flexShrink: 0, ...(active && color ? { background: color, borderColor: color } : {}) }}>
{color && }
{t === 'all' ? 'Mind' : t}
);
})}
{showUpload &&
setShowUpload(false)}/>}
);
};
const DocumentUploadSheet = ({ onClose }) => {
const [file, setFile] = React.useState(null);
const [name, setName] = React.useState('');
const [tag, setTag] = React.useState(Object.keys(window.TAG_COLORS)[0] || '');
const [busy, setBusy] = React.useState(false);
const fileRef = React.useRef(null);
const onPick = (e) => {
const f = e.target.files?.[0];
if (!f) return;
setFile(f);
if (!name) setName(f.name.replace(/\.[^.]+$/, ''));
};
const submit = async () => {
if (!file || !name.trim() || !tag) return;
setBusy(true);
try {
const fd = new FormData();
fd.append('file', file);
fd.append('name', name.trim());
fd.append('tag', tag);
await window.api.documents.upload(fd);
await window.refreshData();
onClose();
} catch (err) {
alert(window.apiErrorMessage(err));
} finally {
setBusy(false);
}
};
return (
fileRef.current?.click()}
style={{ padding: 30, textAlign: 'center', borderStyle: 'dashed', borderColor: 'var(--accent)', display: 'flex', flexDirection: 'column', gap: 8, alignItems: 'center', color: 'var(--accent-d)' }}>
{file ? file.name : 'Fájl választása'}
PDF, JPG, PNG, max 100 MB
Név
setName(e.target.value)} placeholder="Pl. Belsőépítészeti koncepció v4"/>
Címke
{Object.keys(window.TAG_COLORS).map(t => {
const c = window.TAG_COLORS[t];
const active = tag === t;
return (
setTag(t)}
style={{ background: active ? c : c + '22', color: active ? '#fff' : c, border: active ? '1px solid ' + c : 'none' }}>
{t}
);
})}
Mégse
{busy ? 'Feltöltés…' : 'Feltöltés'}
);
};
Object.assign(window, { Dashboard, Timeline, Documents, DocumentUploadSheet, Mini });