// Admin nézet — felhasználók, jogosultságok, költségvetés/scope/időzítés szerkesztés // ─── Edit-mode toggle (közös) ───────────────────────────────────────────── const EditToggle = ({ on, setOn }) => ( ); // ─── Admin Kezdő — projekt áttekintés ───────────────────────────────────── const logCost = (l) => l.numWorkers * l.hoursPerWorker * l.hourlyRate + (l.materialCost || 0); const AdminHome = ({ user, setView }) => { const totalBudget = window.totalBudget(); const totalSpent = window.totalSpent(); const totalPending = window.totalPending(); const totalPaid = window.totalPaid(); const pct = totalBudget ? (totalSpent / totalBudget) * 100 : 0; const today = window.TODAY; const todayMs = new Date(today).getTime(); const sevenDaysAgo = todayMs - 7 * 86400000; // Fázisok const activePhases = window.PHASES.filter(p => { const s = new Date(p.start).getTime(), e = new Date(p.end).getTime(); return s <= todayMs && todayMs <= e; }); const overduePhases = window.PHASES.filter(p => new Date(p.end).getTime() < todayMs) .filter(p => { const tasks = window.PARENT_TASKS.filter(t => t.phaseId === p.id); const total = tasks.reduce((s, t) => s + t.budget, 0); const sp = window.phaseSpent(p.id); return total > 0 && sp / total < 0.95; }); // Munkanapló const pendingLogs = window.WORK_LOGS.filter(l => l.status === 'pending'); const recentLogs = window.WORK_LOGS.slice() .sort((a, b) => b.date.localeCompare(a.date)) .slice(0, 5); const activeContractors = new Set( window.WORK_LOGS.filter(l => new Date(l.date).getTime() >= sevenDaysAgo).map(l => l.contractorId) ); // Kifizetésre vár (jóváhagyott − kifizetett) const totalDebt = window.getContractors().reduce((s, c) => s + Math.max(0, window.contractorBalance(c.id)), 0); return (
Projekt · {user.name}

H1 · áttekintés

{/* Költségvetés szalag */}
Teljes költségvetés
{window.formatHufShort(totalBudget)} Ft
Elköltve
{window.formatHufShort(totalSpent)}
Várakozik
{window.formatHufShort(totalPending)}
Kifizetve
{window.formatHufShort(totalPaid)}
{/* KPI sor */}
{/* Aktív fázisok */}
Aktív fázisok · {activePhases.length}
{activePhases.length === 0 &&
Jelenleg nincs futó fázis.
} {activePhases.map(p => { const tasks = window.PARENT_TASKS.filter(t => t.phaseId === p.id); const total = tasks.reduce((s, t) => s + t.budget, 0); const sp = window.phaseSpent(p.id); const sMs = new Date(p.start).getTime(), eMs = new Date(p.end).getTime(); const elapsed = ((todayMs - sMs) / (eMs - sMs)) * 100; return (
{p.code} {p.name}
{Math.max(0, Math.round((eMs - todayMs) / 86400000))} nap
idő · {Math.round(elapsed)}% költés · {total ? Math.round((sp/total)*100) : 0}%
); })}
{/* Lemaradt fázisok */} {overduePhases.length > 0 && (
{overduePhases.length} fázis lemaradásban
{overduePhases.map(p => ( ))}
)} {/* Legutóbbi aktivitás */}
Legutóbbi aktivitás
{recentLogs.map((l, i) => { const c = window.getUser(l.contractorId); const ph = window.getPhase(l.phaseId); const cost = logCost(l); const statusColor = l.status === 'paid' ? 'var(--ok)' : l.status === 'approved' ? 'var(--accent)' : l.status === 'pending' ? 'var(--warn)' : 'var(--ink-3)'; const statusLabel = l.status === 'paid' ? 'kifizetve' : l.status === 'approved' ? 'jóváhagyva' : l.status === 'pending' ? 'várakozik' : l.status; return (
{c && }
{c ? c.name : '—'}
{ph && {ph.code}} {l.description}
{window.formatHufShort(cost)}
{statusLabel}
); })}
{/* Beállítások szekció — kompakt linkek a 4 admin részhez */}
Beállítások
setView('users')}/> setView('budget')}/> setView('timing')}/> setView('permissions')}/>
); }; const DashStat = ({ label, value, sub, accent = 'ink' }) => { const colors = { ink: 'var(--ink)', warn: '#8A6200', ok: 'var(--ok)' }; return (
{label}
{value}
{sub &&
{sub}
}
); }; const SettingsLink = ({ icon, label, sub, onClick }) => ( ); // (legacy ActionTile — már nem használjuk a kezdőn, de export miatt itt marad) const ActionTile = ({ icon, label, sub, onClick }) => ( ); // ─── Felhasználók ───────────────────────────────────────────────────────── const AdminUsers = () => { const [, force] = React.useReducer(x => x + 1, 0); const [edit, setEdit] = React.useState(false); const [showNew, setShowNew] = React.useState(false); const [editUser, setEditUser] = React.useState(null); const [confirmDel, setConfirmDel] = React.useState(null); return (

Felhasználók

{edit && }
{['admin', 'approver', 'contractor'].map(role => { const roleUsers = window.USERS.filter(u => u.role === role); const labels = { admin: 'Adminok', approver: 'Jóváhagyók', contractor: 'Vállalkozók' }; return (
{labels[role]} · {roleUsers.length}
{roleUsers.map(u => (
{u.name}
{u.email}
{u.skill &&
{u.skill} · {window.formatHufShort(u.hourlyRate)} Ft/óra
}
{edit && }
))}
); })} {showNew && ( setShowNew(false)} onSave={async (data) => { try { await window.api.users.save(data); await window.refreshData(); } catch (err) { alert(window.apiErrorMessage(err)); } setShowNew(false); force(); }}/> )} {editUser && ( setEditUser(null)} onDelete={() => { setConfirmDel(editUser); setEditUser(null); }} onSave={async (data) => { try { await window.api.users.save({ id: editUser.id, ...data }); await window.refreshData(); } catch (err) { alert(window.apiErrorMessage(err)); } // Optimistic local update so the sheet closes responsively even // if the server is slow. Object.assign(editUser, data); setEditUser(null); force(); }}/> )} {confirmDel && ( setConfirmDel(null)} title="Felhasználó törlése">
Biztosan törölni szeretnéd: {confirmDel.name}?
)}
); }; // ─── Költségvetés és scope szerkesztő ───────────────────────────────────── const AdminBudget = () => { const [, force] = React.useReducer(x => x + 1, 0); const [edit, setEdit] = React.useState(false); const [openPhase, 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); return (

Költségvetés és scope

{edit && }
{edit ? 'Fázisok és tételek hozzáadása, szerkesztése, törlése. A változások azonnal megjelennek a vállalkozói és jóváhagyói nézetekben.' : 'Fázisok és tételek áttekintése. Szerkesztéshez kapcsold be a Szerkesztés módot.'}
{window.PHASES.map(p => { const tasks = window.PARENT_TASKS.filter(t => t.phaseId === p.id); const total = tasks.reduce((s, t) => s + t.budget, 0); const spent = window.phaseSpent(p.id); const pct = total > 0 ? (spent / total) * 100 : 0; const isOpen = openPhase === p.id; return (
{isOpen && (
{tasks.length === 0 && (
Még nincs tétel ebben a fázisban.
)} {tasks.map(t => { const sp = window.parentTaskSpent(t.id); return (
{t.name}
{window.formatHufShort(t.budget)} Ft
); })} {edit && (
)}
)}
); })}
{/* Új tétel sheet */} {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(); }}/> )} {/* Tétel szerkesztés */} {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(); }}/> )} {/* Új fázis */} {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(); }}/> )} {/* Fázis szerkesztés */} {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(); }}/> )} {/* Törlés megerősítés */} {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.
)}
)}
); }; const TaskSheet = ({ task, phase, onClose, onSave }) => { const [name, setName] = React.useState(task ? task.name : ''); const [budget, setBudget] = React.useState(task ? task.budget : 0); const valid = name.trim().length > 0 && budget >= 0; return (
{phase.code} · {phase.name}
setName(e.target.value)} placeholder="Pl. Falazás új falak"/>
setBudget(+e.target.value || 0)}/>
{window.formatHuf(budget)} Ft
); }; const PALETTE = ['#6805E1', '#1482FA', '#1AAA9B', '#B2DE78', '#FFBD3B', '#F15922', '#F68069', '#9D4DFF', '#3DC1B5', '#4DA6FF', '#C8E89A', '#FFD370', '#FF8551', '#7DA8FF', '#030630']; const PhaseSheet = ({ phase, onClose, onSave }) => { const [name, setName] = React.useState(phase ? phase.name : ''); const [color, setColor] = React.useState(phase ? phase.color : PALETTE[0]); const [start, setStart] = React.useState(phase ? phase.start : '2026-01-01'); const [end, setEnd] = React.useState(phase ? phase.end : '2026-12-31'); const valid = name.trim().length > 0 && start && end && start <= end; return (
setName(e.target.value)} placeholder="Pl. Tetőszerkezet"/>
setStart(e.target.value)}/>
setEnd(e.target.value)}/>
{PALETTE.map(c => (
); }; // ─── Időzítés (timeline szerkesztő) ─────────────────────────────────────── const AdminTiming = () => { 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); // mini-Gantt domain const allDates = window.PHASES.flatMap(p => [p.start, p.end]).filter(Boolean).sort(); const min = allDates[0] || '2026-01-01'; const max = allDates[allDates.length - 1] || '2026-12-31'; const minMs = new Date(min).getTime(); const maxMs = new Date(max).getTime(); const span = Math.max(1, maxMs - minMs); const todayMs = Date.now(); const todayPct = ((todayMs - minMs) / span) * 100; const monthMarks = []; { const d = new Date(minMs); d.setDate(1); while (d.getTime() <= maxMs) { const pct = ((d.getTime() - minMs) / span) * 100; monthMarks.push({ pct, label: d.toLocaleDateString('hu-HU', { month: 'short' }) }); d.setMonth(d.getMonth() + 1); } } return (

Időzítés

{edit && }
{edit ? 'Módosítsd a kezdő és záró dátumokat. A piros szín jelzi a lemaradt szakaszokat.' : 'Fázisok időzítése és lemaradások. Szerkesztéshez kapcsold be a Szerkesztés módot.'}
{/* Mini-Gantt */}
{/* hónap vonalak */}
{monthMarks.map((m, i) => (
{m.label}
))}
{/* sávok */}
{/* ma vonal */} {todayPct >= 0 && todayPct <= 100 && (
MA
)} {window.PHASES.map(p => { const sMs = new Date(p.start).getTime(); const eMs = new Date(p.end).getTime(); const left = ((sMs - minMs) / span) * 100; const width = Math.max(1, ((eMs - sMs) / span) * 100); const overdue = eMs < todayMs; return ( ); })}
{/* Lista — közvetlen dátum szerkesztés */}
Fázisok időzítése
{window.PHASES.map(p => { const sMs = new Date(p.start).getTime(); const eMs = new Date(p.end).getTime(); const days = Math.round((eMs - sMs) / 86400000) + 1; const overdue = eMs < todayMs; return (
{p.code} {p.name}
{overdue && LEMARADT}
{days} nap
{edit ? { p.start = e.target.value; force(); }} onBlur={async (e) => { try { await window.api.phases.save({ id: p.id, name: p.name, color: p.color, start: e.target.value, end: p.end }); await window.refreshData(); } catch (err) { alert(window.apiErrorMessage(err)); } }}/> :
{p.start}
}
{edit ? { p.end = e.target.value; force(); }} onBlur={async (e) => { try { await window.api.phases.save({ id: p.id, name: p.name, color: p.color, start: p.start, end: e.target.value }); await window.refreshData(); } catch (err) { alert(window.apiErrorMessage(err)); } }}/> :
{p.end}
}
{edit && }
); })}
{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(); }}/> )}
); }; // ─── Jogosultságok ──────────────────────────────────────────────────────── const AdminPermissions = () => { const [, force] = React.useReducer(x => x + 1, 0); const [edit, setEdit] = React.useState(false); const matrix = [ { name: 'Saját teljesítések rögzítése', c: true, ap: true, ad: true }, { name: 'Mások teljesítéseinek megtekintése', c: false, ap: true, ad: true }, { name: 'Teljesítések jóváhagyása', c: false, ap: true, ad: true }, { name: 'Kifizetés rögzítése', c: false, ap: true, ad: true }, { name: 'Számla beolvasás (OCR)', c: false, ap: true, ad: true }, { name: 'Költségvetés megtekintése', c: 'p', ap: true, ad: true }, { name: 'Költségvetés szerkesztése', c: false, ap: false, ad: true }, { name: 'Dashboard megtekintése', c: 'p', ap: true, ad: true }, { name: 'Ütemterv megtekintése', c: true, ap: true, ad: true }, { name: 'Ütemterv szerkesztése', c: false, ap: false, ad: true }, { name: 'Dokumentum feltöltés', c: true, ap: true, ad: true }, { name: 'Felhasználók kezelése', c: false, ap: false, ad: true }, { name: 'Jogosultságok beállítása', c: false, ap: false, ad: true }, ]; return (

Jogosultságok

{edit ? 'Kattints egy cellára a jogosultság váltásához (✓ → részleges → —).' : 'Szerep szerinti alapértékek. Finomhangoláshoz kapcsold be a Szerkesztés módot, vagy a Felhasználók nézetben állítsd egyénenként.'}
Funkció
Vállalkozó
Jóváhagyó
Admin
{matrix.map((m, i) => { const cycle = (key) => { const v = m[key]; m[key] = v === true ? 'p' : v === 'p' ? false : true; force(); }; return (
{m.name}
cycle('c')}/> cycle('ap')}/> cycle('ad')}/>
); })}
A 'p' = részleges (pl. csak saját adatok láthatók). Az adminisztrátor finomhangolhatja egyénileg, hogy egy-egy vállalkozó láthassa-e a Dashboardot.
); }; const PermCell = ({ v, edit, onClick }) => { const inner = v === true ? : v === 'p' ? részleges : ; if (edit) { return ( ); } return
{inner}
; }; const ROLE_OPTIONS = [ { v: 'contractor', l: 'Vállalkozó' }, { v: 'approver', l: 'Jóváhagyó' }, { v: 'admin', l: 'Admin' }, ]; const UserSheet = ({ user, onClose, onSave, onDelete }) => { const [name, setName] = React.useState(user ? user.name : ''); const [email, setEmail] = React.useState(user ? user.email : ''); const [phone, setPhone] = React.useState(user ? user.phone || '' : ''); const [role, setRole] = React.useState(user ? user.role : 'contractor'); const [skill, setSkill] = React.useState(user ? user.skill || '' : ''); const [hourlyRate, setHourlyRate] = React.useState(user ? user.hourlyRate || 0 : 5000); const [password, setPassword] = React.useState(''); const [showPwd, setShowPwd] = React.useState(false); const valid = name.trim().length > 0 && email.trim().length > 0 && (!password || password.length >= 6); return (
setName(e.target.value)} placeholder="Pl. Kovács Anna"/>
setEmail(e.target.value)} placeholder="email@példa.hu"/>
setPhone(e.target.value)}/>
{role === 'contractor' && ( <>
setSkill(e.target.value)} placeholder="Pl. Kőműves"/>
setHourlyRate(+e.target.value || 0)}/>
)}
setPassword(e.target.value)} placeholder={user ? '••••••••' : 'Új jelszó (min. 6 karakter)'} autoComplete="new-password" style={{ flex: 1 }}/>
{!user && !password && (
Üresen hagyva alapértelmezett jelszó: h1demo
)} {password && password.length > 0 && password.length < 6 && (
Min. 6 karakter
)}
{user && onDelete && ( )}
); }; Object.assign(window, { AdminHome, AdminUsers, AdminBudget, AdminTiming, AdminPermissions, ActionTile, PermCell, TaskSheet, PhaseSheet, UserSheet });