import { useCallback, useEffect, useMemo, useState } from "react"; import { ArrowUpRight, Plus, Users2, Star } from "lucide-react"; import { api } from "../api.js"; import { Badge, Button, cx, Dialog, Empty, ErrorNote, Field, fmt, fmtDate, Panel, Spinner, Stat, StatCell, StatRow } from "../ui.jsx"; export default function Team() { const [data, setData] = useState(null); const [error, setError] = useState(null); const [person, setPerson] = useState(""); const [project, setProject] = useState(""); const [overdueOnly, setOverdueOnly] = useState(false); const [q, setQ] = useState(""); const [creating, setCreating] = useState(false); const load = useCallback(() => api("/api/team").then(setData).catch(setError), []); useEffect(() => { load(); }, [load]); const tasks = useMemo(() => { const term = q.trim().toLowerCase(); return (data?.tasks || []).filter( (t) => (!person || t.assignees.some((a) => String(a.id) === person)) && (!project || String(t.project?.id) === project) && (!overdueOnly || t.overdue) && (!term || t.name.toLowerCase().includes(term) || t.project?.name.toLowerCase().includes(term)) ); }, [data, person, project, overdueOnly, q]); if (error) return ; if (!data) return ; if (!data.status.connected) { return (

Team

{data.status.error || "An admin can connect it in Connections → Odoo (address, database, username and API key)."}
); } const all = data.tasks; const overdue = all.filter((t) => t.overdue).length; const unassigned = all.filter((t) => !t.assignees.length).length; const usedProjects = data.projects.filter((p) => all.some((t) => t.project?.id === p.id)); const busiest = data.workload.reduce((m, w) => Math.max(m, w.open), 0); return (

Team

Live from {data.status.url?.replace(/^https?:\/\//, "")} · Odoo {data.status.version?.split("-")[0]}

{data.status.allowWrites && }
    {data.workload.map((w) => (
  • ))}
setQ(e.target.value)} />
} bodyClass="overflow-x-auto" > {tasks.length ? ( {tasks.map((t) => ( ))}
TaskAssigneeStageDeadline
{t.priority === "1" && } {t.name}
{t.project?.name || "No project"}
{t.assignees.map((a) => a.name).join(", ") || unassigned} {t.stage?.name || "—"} {t.deadline ? fmtDate(t.deadline.replace(" ", "T") + "Z") : "—"} {t.overdue && overdue}
) : ( Clear the filters to see every open task. )}
setCreating(false)} data={data} onCreated={load} /> ); } function NewTaskDialog({ open, onClose, data, onCreated }) { const [form, setForm] = useState({ projectId: "", name: "", description: "", userIds: [], deadline: "" }); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); useEffect(() => { if (open) { setForm({ projectId: "", name: "", description: "", userIds: [], deadline: "" }); setError(null); } }, [open]); const set = (k) => (e) => setForm((f) => ({ ...f, [k]: e.target.value })); const toggleUser = (id) => setForm((f) => ({ ...f, userIds: f.userIds.includes(id) ? f.userIds.filter((x) => x !== id) : [...f.userIds, id] })); const submit = async () => { setBusy(true); setError(null); try { await api("/api/team/tasks", { method: "POST", body: form }); onClose(); onCreated(); } catch (e) { setError(e); } finally { setBusy(false); } }; return ( } >