// Feladatkezelő — projekt-szintű, mindenki által látható közös feladattábla.
// A felső sávban a korábbi csapat chat helyét veszi át. Minden szerep látja
// és szerkesztheti; törölni csak a létrehozó vagy admin tud.
const TASK_STATUSES = [
{ key: 'todo', label: 'Teendő', cls: 'todo' },
{ key: 'in_progress', label: 'Folyamatban', cls: 'doing' },
{ key: 'done', label: 'Kész', cls: 'done' },
];
const TASK_PRIORITIES = [
{ key: 'high', label: 'Magas', cls: 'high' },
{ key: 'normal', label: 'Normál', cls: 'normal' },
{ key: 'low', label: 'Alacsony', cls: 'low' },
];
const taskStatusMeta = (k) => TASK_STATUSES.find(s => s.key === k) || TASK_STATUSES[0];
const taskPriorityMeta = (k) => TASK_PRIORITIES.find(p => p.key === k) || TASK_PRIORITIES[1];
// ─── Létrehozó / szerkesztő űrlap ──────────────────────────────────────────
const TaskForm = ({ user, task, onClose, onSaved, onDeleted, onToast }) => {
const editing = !!task;
const [title, setTitle] = React.useState(task?.title || '');
const [description, setDescription] = React.useState(task?.description || '');
const [assigneeId, setAssigneeId] = React.useState(task?.assigneeId ? String(task.assigneeId) : '');
const [status, setStatus] = React.useState(task?.status || 'todo');
const [priority, setPriority] = React.useState(task?.priority || 'normal');
const [dueDate, setDueDate] = React.useState(task?.dueDate || '');
const [tagText, setTagText] = React.useState((task?.tags || []).join(', '));
const [busy, setBusy] = React.useState(false);
const canDelete = editing && (task.createdBy === user.id || user.role === 'admin');
const members = window.USERS || [];
const submit = async () => {
if (!title.trim()) { onToast('Adj címet a feladatnak'); return; }
setBusy(true);
try {
const payload = {
title: title.trim(),
description: description.trim(),
assignee_id: assigneeId ? Number(assigneeId) : null,
status,
priority,
due_date: dueDate || '',
tags: tagText,
};
if (editing) payload.id = task.id;
await window.api.tasks.save(payload);
await window.refreshData();
onSaved(editing);
} catch (err) {
onToast('Hiba: ' + window.apiErrorMessage(err));
} finally {
setBusy(false);
}
};
const remove = async () => {
if (!window.confirm('Biztosan törlöd ezt a feladatot?')) return;
setBusy(true);
try {
await window.api.tasks.del(task.id);
await window.refreshData();
onDeleted();
} catch (err) {
onToast('Hiba: ' + window.apiErrorMessage(err));
} finally {
setBusy(false);
}
};
return (
setTitle(e.target.value)}
placeholder="Mi a teendő?" autoFocus maxLength={255}/>
setDueDate(e.target.value)}/>
{canDelete && (
)}
);
};
// ─── Egy feladat kártya ──────────────────────────────────────────────────
const TaskCard = ({ task, onEdit, onAdvance, onDragStart, onDragEnd, dragging }) => {
const assignee = task.assigneeId ? window.getUser(task.assigneeId) : null;
const prio = taskPriorityMeta(task.priority);
const sMeta = taskStatusMeta(task.status);
const isDone = task.status === 'done';
const overdue = task.dueDate && !isDone && task.dueDate < window.TODAY;
return (
);
};
// ─── Fő tábla (overlay) ─────────────────────────────────────────────────────
const Tasks = ({ user, onClose, onToast }) => {
const [, force] = React.useReducer(x => x + 1, 0);
const [query, setQuery] = React.useState('');
const [statusFilter, setStatusFilter] = React.useState('todo'); // todo | in_progress | done | all
const [mineOnly, setMineOnly] = React.useState(false);
const [form, setForm] = React.useState(null); // null | { task: Task|null }
const [dragId, setDragId] = React.useState(null); // épp húzott feladat id-ja
const [dropStatus, setDropStatus] = React.useState(null); // a célzott állapot-chip kulcsa
const all = window.TASKS || [];
const openNew = () => setForm({ task: null });
const openEdit = (t) => setForm({ task: t });
const closeForm = () => setForm(null);
// Léptetés a kártyáról: todo → folyamatban → kész → todo.
const advance = async (t) => {
const order = ['todo', 'in_progress', 'done'];
const next = order[(order.indexOf(t.status) + 1) % order.length];
t.status = next; // optimista helyi váltás
force();
try {
await window.api.tasks.status({ id: t.id, status: next });
await window.refreshData();
} catch (err) {
onToast('Hiba: ' + window.apiErrorMessage(err));
await window.refreshData();
}
};
// Húzás egy állapot-chipre: a feladat oda kerül — ugyanúgy, mint a
// dokumentumoknál a címkére húzás. Optimista helyi váltás, majd szinkron.
const moveToStatus = async (id, status) => {
const t = all.find(x => x.id === id);
if (!t || t.status === status) return;
t.status = status;
force();
try {
await window.api.tasks.status({ id, status });
await window.refreshData();
} catch (err) {
onToast('Hiba: ' + window.apiErrorMessage(err));
await window.refreshData();
}
};
const q = query.trim().toLowerCase();
const filtered = all.filter(t => {
if (mineOnly && t.assigneeId !== user.id) return false;
if (statusFilter !== 'all' && t.status !== statusFilter) return false;
if (q) {
const hay = [
t.title,
t.description,
(t.tags || []).join(' '),
window.getUser(t.assigneeId)?.name || '',
].join(' ').toLowerCase();
if (!hay.includes(q)) return false;
}
return true;
});
// Csoporton belüli rendezés: prioritás, majd határidő, majd létrehozás.
const prioRank = { high: 0, normal: 1, low: 2 };
const sortTasks = (list) => list.slice().sort((a, b) => {
if (prioRank[a.priority] !== prioRank[b.priority]) return prioRank[a.priority] - prioRank[b.priority];
const ad = a.dueDate || '9999-12-31', bd = b.dueDate || '9999-12-31';
if (ad !== bd) return ad < bd ? -1 : 1;
return a.id - b.id;
});
const groups = TASK_STATUSES.map(s => ({
...s,
items: sortTasks(filtered.filter(t => t.status === s.key)),
}));
const counts = {
all: all.length,
todo: all.filter(t => t.status === 'todo').length,
in_progress: all.filter(t => t.status === 'in_progress').length,
done: all.filter(t => t.status === 'done').length,
};
const boardEmpty = all.length === 0;
return (
{/* Fejléc */}
Feladatok
{counts.all} feladat · közös tábla
{/* Eszköztár: keresés + szűrők */}
{/* Lista, állapot szerint csoportosítva */}
{filtered.length === 0 ? (
Nincs feladat
{boardEmpty
? 'Hozd létre az első feladatot a jobb felső „Új” gombbal.'
: 'Nincs a szűrőnek megfelelő feladat.'}
) : groups.map(g => {
if (g.items.length === 0) return null;
return (
{g.label}
{g.items.length}
{g.items.map(t => (
openEdit(t)}
onAdvance={() => advance(t)}
dragging={dragId === t.id}
onDragStart={(e) => { e.dataTransfer.setData('text/plain', String(t.id)); e.dataTransfer.effectAllowed = 'move'; setDragId(t.id); }}
onDragEnd={() => { setDragId(null); setDropStatus(null); }}/>
))}
);
})}
{form && (
{ closeForm(); onToast(editing ? '✓ Feladat mentve' : '✓ Feladat létrehozva'); }}
onDeleted={() => { closeForm(); onToast('✓ Feladat törölve'); }}
onToast={onToast}
/>
)}
);
};
window.Tasks = Tasks;
window.TaskForm = TaskForm;
window.TaskCard = TaskCard;