163 lines
5.2 KiB
TypeScript
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> 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>
|
|
);
|
|
}
|