// 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 }) => (
setOn(!on)}
style={{ gap: 6 }}
aria-pressed={on}>
{on ? 'Kész' : 'Szerkesztés'}
);
// ─── 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}
setView('timeline')} style={{ fontSize: 11, color: 'var(--ink-3)' }}>Ütemterv →
{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 => (
setView('timing')}
style={{ background: 'var(--bg)', border: '1px solid rgba(220,38,38,0.25)', color: 'var(--bad)', fontSize: 11, fontWeight: 500 }}>
{p.code} {p.name}
))}
)}
{/* 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 && setShowNew(true)}> Új }
{['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 && setEditUser(u)} aria-label="Szerkesztés"> }
))}
);
})}
{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} ?
setConfirmDel(null)}>Mégse
{
try {
await window.api.users.del(confirmDel.id);
await window.refreshData();
} catch (err) { alert(window.apiErrorMessage(err)); }
const i = window.USERS.findIndex(x => x.id === confirmDel.id);
if (i >= 0) window.USERS.splice(i, 1);
setConfirmDel(null); force();
}}>Törlés
)}
);
};
// ─── 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 && setNewPhase(true)}> Új fázis }
{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 (
setOpenPhase(isOpen ? null : p.id)}
style={{ flex: 1, minWidth: 0, textAlign: 'left' }}>
{p.code}
{p.name}
{window.formatHufShort(total)} Ft
{Math.round(pct)}%
setEditPhase(p)} style={{ padding: 6, display: edit ? '' : 'none' }}>
setConfirmDel({ kind: 'phase', target: p })} style={{ padding: 6, display: edit ? '' : 'none' }}>
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, display: edit ? '' : 'none' }}>
setConfirmDel({ kind: 'task', target: t })} style={{ padding: 6, display: edit ? '' : 'none' }}>
);
})}
{edit && (
setNewTaskFor(p)}> Új tétel
)}
)}
);
})}
{/* Ú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.
)}
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 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}
Tétel neve
setName(e.target.value)} placeholder="Pl. Falazás új falak"/>
Mégse
onSave({ name: name.trim(), budget })}>Mentés
);
};
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 (
Fázis neve
setName(e.target.value)} placeholder="Pl. Tetőszerkezet"/>
Szín
{PALETTE.map(c => (
setColor(c)} style={{
width: 28, height: 28, borderRadius: 8, background: c,
border: color === c ? '2px solid var(--ink)' : '2px solid transparent',
boxShadow: color === c ? '0 0 0 2px var(--bg) inset' : 'none'
}}/>
))}
Mégse
onSave({ name: name.trim(), color, start, end })}>Mentés
);
};
// ─── 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 && setNewPhase(true)}> Új fázis }
{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 && (
)}
{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 (
setEditPhase(p)}
style={{ display: 'flex', alignItems: 'center', gap: 8, width: '100%', padding: '6px 0', textAlign: 'left' }}>
{p.code} {p.name}
);
})}
{/* 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
Kezdés
{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}
}
Vége
{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 &&
setEditPhase(p)} style={{ padding: 6, alignSelf: 'flex-end' }} title="Részletek"> }
);
})}
{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 (
{inner}
);
}
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 (
);
};
Object.assign(window, { AdminHome, AdminUsers, AdminBudget, AdminTiming, AdminPermissions, ActionTile, PermCell, TaskSheet, PhaseSheet, UserSheet });