Files
ulweb/web/pages/index.tsx
2026-08-20 22:00:38 -05:00

163 lines
5.2 KiB
TypeScript

import Link from "next/link";
import { useEffect, useState } from "react";
import Layout from "../components/Layout";
import { humanizeStatus, PageState, StatusBadge } from "../components/PortalUi";
import { api } from "../lib/api";
import { useRequireAuth } from "../lib/auth-context";
interface JobRow {
id: string;
ticketNumber: string;
title: string;
address: string | null;
status: string;
source: string;
createdAt: string;
assignedTo: { id: string; name: string } | null;
_count: { points: number };
}
const STATUSES = ["", "OPEN", "IN_PROGRESS", "COMPLETED", "CANCELLED"];
export default function JobsPage() {
const { user, activeOrg, loading } = useRequireAuth();
const [jobs, setJobs] = useState<JobRow[]>([]);
const [total, setTotal] = useState(0);
const [status, setStatus] = useState("");
const [q, setQ] = useState("");
const [error, setError] = useState<string | null>(null);
const [fetching, setFetching] = useState(false);
useEffect(() => {
if (!activeOrg) {
return;
}
let cancelled = false;
const params = new URLSearchParams();
if (status) params.set("status", status);
if (q) params.set("q", q);
setFetching(true);
api
.get<{ jobs: JobRow[]; total: number }>(
`/api/orgs/${activeOrg.org.id}/jobs?${params}`,
)
.then((res) => {
if (cancelled) return;
setJobs(res.jobs);
setTotal(res.total);
setError(null);
})
.catch((err) => {
if (!cancelled) setError(err.message);
})
.finally(() => {
if (!cancelled) setFetching(false);
});
return () => {
cancelled = true;
};
}, [activeOrg, status, q]);
if (loading || !user) {
return <PageState kind="loading" title="Loading your workspace…" />;
}
return (
<Layout title="Jobs">
<header className="ul-page-header">
<div className="ul-page-header__title-group">
<span className="ul-page-header__eyebrow">Operations</span>
<h1>Jobs</h1>
<p className="ul-page-header__meta" aria-live="polite">
{fetching
? "Updating jobs…"
: `${total} ${total === 1 ? "job" : "jobs"}`}
</p>
</div>
<Link href="/jobs/new" className="ul-button">
<span aria-hidden="true">+</span>&nbsp; New job
</Link>
</header>
<section className="ul-panel ul-filter-bar" aria-label="Job filters">
<label className="ul-field">
<span className="ul-field__label">Search jobs</span>
<input
type="search"
placeholder="Ticket, title, or address"
value={q}
onChange={(event) => setQ(event.target.value)}
/>
</label>
<label className="ul-field">
<span className="ul-field__label">Status</span>
<select
value={status}
onChange={(event) => setStatus(event.target.value)}
>
{STATUSES.map((item) => (
<option key={item} value={item}>
{item ? humanizeStatus(item) : "All statuses"}
</option>
))}
</select>
</label>
</section>
{error ? (
<PageState kind="error" title="Jobs could not be loaded">
{error}
</PageState>
) : jobs.length === 0 && !fetching ? (
<PageState kind="empty" title="No jobs found">
{q || status
? "Try clearing or changing the current filters."
: "Create a job, or let a field device post points to auto-create its ticket."}
</PageState>
) : (
<div className="ul-panel ul-table-wrap" aria-busy={fetching}>
<table className="ul-table">
<caption className="ul-visually-hidden">
Jobs for the active organization
</caption>
<thead>
<tr>
<th scope="col">Ticket</th>
<th scope="col">Title</th>
<th scope="col">Address</th>
<th scope="col">Status</th>
<th scope="col">Source</th>
<th scope="col">Assignee</th>
<th scope="col" className="ul-table__numeric">
Points
</th>
</tr>
</thead>
<tbody>
{jobs.map((job) => (
<tr key={job.id}>
<td data-label="Ticket">
<Link className="ul-ticket-link" href={`/jobs/${job.id}`}>
{job.ticketNumber}
</Link>
</td>
<td data-label="Title">{job.title}</td>
<td data-label="Address">{job.address ?? "—"}</td>
<td data-label="Status">
<StatusBadge status={job.status} />
</td>
<td data-label="Source">{humanizeStatus(job.source)}</td>
<td data-label="Assignee">{job.assignedTo?.name ?? "—"}</td>
<td data-label="Points" className="ul-table__numeric">
{job._count.points}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</Layout>
);
}