/* TargiApp — harmonogram: kalendarz tygodniowy (kolumny = dni), karty = osoby z rolą/godzinami/notatką. Drag&drop kart między dniami + przeciąganie z listy zespołu + dropdown "Dodaj osobę". Przełączanie tygodni, filtrowanie (rola + osoba) i sortowanie kart. */ // ---- helpers dat / tygodnia ---- const MONTHS_SHORT = ["sty", "lut", "mar", "kwi", "maj", "cze", "lip", "sie", "wrz", "paź", "lis", "gru"]; function isoDate(d) { return d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0"); } function mondayOf(dateStr) { const d = new Date(dateStr); const off = (d.getDay() + 6) % 7; d.setHours(0, 0, 0, 0); d.setDate(d.getDate() - off); return d; } function addDays(date, n) { const d = new Date(date); d.setDate(d.getDate() + n); return d; } function AssignCard({ a, onEdit, onCardDrop }) { const store = useStore(); const p = store.personById(a.personId); const r = store.roleOf(a.role); const [edge, setEdge] = useState(null); // 'top' | 'bottom' — wskaźnik miejsca wstawienia return (
{ e.dataTransfer.setData("text/assignment", String(a.id)); e.dataTransfer.effectAllowed = "move"; e.currentTarget.classList.add("dragging"); }} onDragEnd={(e) => e.currentTarget.classList.remove("dragging")} onDragOver={(e) => { e.preventDefault(); const rect = e.currentTarget.getBoundingClientRect(); setEdge(e.clientY < rect.top + rect.height / 2 ? "top" : "bottom"); }} onDragLeave={() => setEdge(null)} onDrop={(e) => { e.preventDefault(); e.stopPropagation(); const before = edge !== "bottom"; setEdge(null); onCardDrop(e, a, before); }} onClick={() => onEdit(a)} >
{store.fullName(p)}
{a.from}–{a.to}
{a.note ?
{a.note}
: null}
); } function AddPersonDropdown({ date }) { const store = useStore(); const [open, setOpen] = useState(false); const [q, setQ] = useState(""); const ref = useRef(null); useEffect(() => { if (!open) return; function onDoc(e) { if (ref.current && !ref.current.contains(e.target)) setOpen(false); } document.addEventListener("mousedown", onDoc); return () => document.removeEventListener("mousedown", onDoc); }, [open]); const available = store.state.people .filter((p) => store.fullName(p).toLowerCase().includes(q.toLowerCase())) .sort((a, b) => store.fullName(a).localeCompare(store.fullName(b))); return (
{open ? (
🔍 setQ(e.target.value)} />
{available.map((p) => (
{ store.addAssignment(p.id, date); setOpen(false); }}> {store.fullName(p)}
))} {!available.length ?
Brak dostępnych osób
: null}
) : null}
); } function DayColumn({ day, onEdit, roleFilter, personQuery, sortBy, setSortBy }) { const store = useStore(); const [over, setOver] = useState(false); const roleOrder = store.ROLES.map((r) => r.key); const q = personQuery.trim().toLowerCase(); let items = store.assignmentsForDay(day.date) .filter((a) => roleFilter.has(a.role)) .filter((a) => !q || store.fullName(store.personById(a.personId)).toLowerCase().includes(q)); if (sortBy !== "reczna") { items = items.slice().sort((a, b) => { if (sortBy === "rola") return roleOrder.indexOf(a.role) - roleOrder.indexOf(b.role) || (a.from || "").localeCompare(b.from || ""); if (sortBy === "nazwisko") { const pa = store.personById(a.personId), pb = store.personById(b.personId); return (pa.lastName || "").localeCompare(pb.lastName || ""); } return (a.from || "").localeCompare(b.from || ""); // godzina }); } // w trybie "reczna" zostaje kolejność wg pozycji (z assignmentsForDay) function onDrop(e) { e.preventDefault(); setOver(false); const aId = e.dataTransfer.getData("text/assignment"); const pId = e.dataTransfer.getData("text/person"); if (aId) store.moveAssignment(Number(aId), day.date); else if (pId) store.addAssignment(Number(pId), day.date); } // upuszczenie na konkretną kartę = ustawienie kolejności (przełącza na tryb ręczny) function onCardDrop(e, target, before) { const aId = e.dataTransfer.getData("text/assignment"); const pId = e.dataTransfer.getData("text/person"); if (aId) { const draggedId = Number(aId); if (draggedId === target.id) return; const dragged = store.state.assignments.find((x) => x.id === draggedId); if (dragged && dragged.date !== day.date) store.moveAssignment(draggedId, day.date); store.reorderAssignment(draggedId, target.id, before); setSortBy("reczna"); } else if (pId) { Promise.resolve(store.addAssignment(Number(pId), day.date)).then((created) => { if (created) { store.reorderAssignment(created.id, target.id, before); setSortBy("reczna"); } }); } } const cls = "daycol" + (over ? " drop" : "") + (day.isFair ? " fair" : "") + (day.weekend ? " weekend" : "") + (!day.isFair && !items.length ? " dim" : ""); const user = store.currentUser(); return (
{ e.preventDefault(); setOver(true); }} onDragLeave={() => setOver(false)} onDrop={onDrop}>
{items.length} os.
{day.dow} {day.dom}
{day.monthName}
{user && user.isAdmin ? (
{day.isFair ? ( store.setFairDayLabel(day.date, e.target.value)} /> ) : null}
) : (day.isFair ? {day.fairLabel || "Targi"} : null)}
{items.map((a) => )} {!items.length ?
Przeciągnij osobę tutaj
lub dodaj poniżej
: null}
); } function Roster() { const store = useStore(); const [q, setQ] = useState(""); const [sort, setSort] = useState("alfa"); let people = store.state.people.filter((p) => store.fullName(p).toLowerCase().includes(q.toLowerCase())); people = people.sort((a, b) => sort === "dni" ? store.daysForPerson(b.id).size - store.daysForPerson(a.id).size || store.fullName(a).localeCompare(store.fullName(b)) : store.fullName(a).localeCompare(store.fullName(b)) ); return ( ); } function EditModal({ a, onClose }) { const store = useStore(); const p = store.personById(a.personId); const [role, setRole] = useState(a.role); const [from, setFrom] = useState(a.from); const [to, setTo] = useState(a.to); const [note, setNote] = useState(a.note); function save() { store.updateAssignment(a.id, { role, from, to, note }); onClose(); } return (
{ if (e.target === e.currentTarget) onClose(); }}>
{store.fullName(p)}
{fmtDayLong(a.date)}
{store.ROLES.map((r) => ( ))}
setFrom(e.target.value)} />
setTo(e.target.value)} />
); } function Toolbar({ weekStart, setWeekStart, weekDays, fairWeekStart, roleFilter, setRoleFilter, sortBy, setSortBy, search, setSearch }) { const store = useStore(); const last = weekDays[6]; const label = weekDays[0].dom + " " + weekDays[0].monthName.split(" ")[0] + " – " + last.dom + " " + last.monthName; function toggleRole(k) { setRoleFilter((prev) => { const next = new Set(prev); if (next.has(k)) next.delete(k); else next.add(k); return next; }); } const allOn = roleFilter.size === store.ROLES.length; return (
Tydzień{label}
Role {store.ROLES.map((r) => ( ))}
🔍 setSearch(e.target.value)} />
); } function SchedulePage() { const store = useStore(); const user = store.currentUser(); const [editing, setEditing] = useState(null); const days0 = (store.state.fair.days && store.state.fair.days[0]) ? store.state.fair.days[0].date : isoDate(new Date()); const fairWeekStart = mondayOf(days0); const [weekStart, setWeekStart] = useState(() => mondayOf(days0)); const [roleFilter, setRoleFilter] = useState(() => new Set(store.ROLES.map((r) => r.key))); const [sortBy, setSortBy] = useState("reczna"); const [search, setSearch] = useState(""); // liczone inline (reaguje na zmiany dni targowych przez admina) const fairMap = {}; (store.state.fair.days || []).forEach((d) => (fairMap[d.date] = d.label)); const dow = ["niedz.", "pon.", "wt.", "śr.", "czw.", "pt.", "sob."]; const weekDays = []; for (let i = 0; i < 7; i++) { const d = addDays(weekStart, i); const ds = isoDate(d); weekDays.push({ date: ds, dow: dow[d.getDay()], dom: d.getDate(), monthName: MONTHS_SHORT[d.getMonth()] + " " + d.getFullYear(), weekend: d.getDay() === 0 || d.getDay() === 6, isFair: Object.prototype.hasOwnProperty.call(fairMap, ds), fairLabel: fairMap[ds] || "", }); } const fairDaysCount = (store.state.fair.days || []).length; const total = store.state.assignments.length; const peopleOnFair = new Set(store.state.assignments.map((a) => a.personId)).size; return (
{user.isAdmin ? store.setFairName(e.target.value)} /> :
{store.state.fair.name}
}
📍 {store.state.fair.venue} · {fairDaysCount} dni targowych · {peopleOnFair} osób w grafiku · {total} przydziałów
{weekDays.map((day) => ( ))}
{editing ? setEditing(null)} /> : null}
); } Object.assign(window, { SchedulePage });