Fix frontend types and normalize API responses
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import styles from "./Shell.module.css";
|
||||
|
||||
const NAV = [
|
||||
@@ -15,16 +15,14 @@ const NAV = [
|
||||
|
||||
export function Shell({ children }: { children: React.ReactNode }) {
|
||||
const path = usePathname();
|
||||
const [actorId, setActorId] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const [actorId, setActorId] = useState(() => {
|
||||
if (typeof window === "undefined") return "";
|
||||
try {
|
||||
const stored = window.localStorage.getItem("actor_employee_id");
|
||||
if (stored) setActorId(stored);
|
||||
return window.localStorage.getItem("actor_employee_id") ?? "";
|
||||
} catch {
|
||||
// ignore
|
||||
return "";
|
||||
}
|
||||
}, []);
|
||||
});
|
||||
return (
|
||||
<div className={styles.shell}>
|
||||
<aside className={styles.sidebar}>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { normalizeDepartments, normalizeEmployees } from "@/lib/normalize";
|
||||
import { Select } from "@/components/ui/select";
|
||||
|
||||
import {
|
||||
@@ -19,8 +20,11 @@ export default function DepartmentsPage() {
|
||||
const [headId, setHeadId] = useState<string>("");
|
||||
|
||||
const departments = useListDepartmentsDepartmentsGet();
|
||||
const departmentList = normalizeDepartments(departments.data);
|
||||
const employees = useListEmployeesEmployeesGet();
|
||||
|
||||
const employeeList = normalizeEmployees(employees.data);
|
||||
|
||||
const createDepartment = useCreateDepartmentDepartmentsPost({
|
||||
mutation: {
|
||||
onSuccess: () => {
|
||||
@@ -37,9 +41,7 @@ export default function DepartmentsPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const sortedEmployees = useMemo(() => {
|
||||
return (employees.data ?? []).slice().sort((a, b) => (a.name ?? "").localeCompare(b.name ?? ""));
|
||||
}, [employees.data]);
|
||||
const sortedEmployees = employeeList.slice().sort((a, b) => (a.name ?? "").localeCompare(b.name ?? ""));
|
||||
|
||||
return (
|
||||
<main className="mx-auto max-w-5xl p-6">
|
||||
@@ -91,7 +93,7 @@ export default function DepartmentsPage() {
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>All departments</CardTitle>
|
||||
<CardDescription>{(departments.data ?? []).length} total</CardDescription>
|
||||
<CardDescription>{departmentList.length} total</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{departments.isLoading ? <div className="text-sm text-muted-foreground">Loading…</div> : null}
|
||||
@@ -100,7 +102,7 @@ export default function DepartmentsPage() {
|
||||
) : null}
|
||||
{!departments.isLoading && !departments.error ? (
|
||||
<ul className="space-y-2">
|
||||
{(departments.data ?? []).map((d) => (
|
||||
{departmentList.map((d) => (
|
||||
<li key={d.id ?? d.name} className="rounded-md border p-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="font-medium">{d.name}</div>
|
||||
@@ -127,7 +129,7 @@ export default function DepartmentsPage() {
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
{(departments.data ?? []).length === 0 ? (
|
||||
{departmentList.length === 0 ? (
|
||||
<li className="text-sm text-muted-foreground">No departments yet.</li>
|
||||
) : null}
|
||||
</ul>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { normalizeAgentOnboardings, normalizeDepartments, normalizeEmployees, normalizeEmploymentActions, normalizeHeadcountRequests } from "@/lib/normalize";
|
||||
import { Select } from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
|
||||
@@ -21,11 +22,16 @@ import { useListDepartmentsDepartmentsGet, useListEmployeesEmployeesGet } from "
|
||||
|
||||
export default function HRPage() {
|
||||
const departments = useListDepartmentsDepartmentsGet();
|
||||
const departmentList = normalizeDepartments(departments.data);
|
||||
const employees = useListEmployeesEmployeesGet();
|
||||
const employeeList = normalizeEmployees(employees.data);
|
||||
|
||||
const headcount = useListHeadcountRequestsHrHeadcountGet();
|
||||
const actions = useListEmploymentActionsHrActionsGet();
|
||||
const onboarding = useListAgentOnboardingHrOnboardingGet();
|
||||
const headcountList = normalizeHeadcountRequests(headcount.data);
|
||||
const actionList = normalizeEmploymentActions(actions.data);
|
||||
const onboardingList = normalizeAgentOnboardings(onboarding.data);
|
||||
|
||||
const [hcDeptId, setHcDeptId] = useState<string>("");
|
||||
const [hcManagerId, setHcManagerId] = useState<string>("");
|
||||
@@ -88,14 +94,6 @@ export default function HRPage() {
|
||||
},
|
||||
});
|
||||
|
||||
mutation: {
|
||||
onSuccess: () => {
|
||||
setActNotes("");
|
||||
actions.refetch();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<main className="mx-auto max-w-5xl p-6">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
@@ -117,13 +115,13 @@ export default function HRPage() {
|
||||
<CardContent className="space-y-3">
|
||||
<Select value={hcDeptId} onChange={(e) => setHcDeptId(e.target.value)}>
|
||||
<option value="">Select department</option>
|
||||
{(departments.data ?? []).map((d) => (
|
||||
{departmentList.map((d) => (
|
||||
<option key={d.id ?? d.name} value={d.id ?? ""}>{d.name}</option>
|
||||
))}
|
||||
</Select>
|
||||
<Select value={hcManagerId} onChange={(e) => setHcManagerId(e.target.value)}>
|
||||
<option value="">Requesting manager</option>
|
||||
{(employees.data ?? []).map((e) => (
|
||||
{employeeList.map((e) => (
|
||||
<option key={e.id ?? e.name} value={e.id ?? ""}>{e.name}</option>
|
||||
))}
|
||||
</Select>
|
||||
@@ -167,13 +165,13 @@ export default function HRPage() {
|
||||
<CardContent className="space-y-3">
|
||||
<Select value={actEmployeeId} onChange={(e) => setActEmployeeId(e.target.value)}>
|
||||
<option value="">Employee</option>
|
||||
{(employees.data ?? []).map((e) => (
|
||||
{employeeList.map((e) => (
|
||||
<option key={e.id ?? e.name} value={e.id ?? ""}>{e.name}</option>
|
||||
))}
|
||||
</Select>
|
||||
<Select value={actIssuerId} onChange={(e) => setActIssuerId(e.target.value)}>
|
||||
<option value="">Issued by</option>
|
||||
{(employees.data ?? []).map((e) => (
|
||||
{employeeList.map((e) => (
|
||||
<option key={e.id ?? e.name} value={e.id ?? ""}>{e.name}</option>
|
||||
))}
|
||||
</Select>
|
||||
@@ -214,13 +212,13 @@ export default function HRPage() {
|
||||
<div>
|
||||
<div className="mb-2 text-sm font-medium">Headcount requests</div>
|
||||
<ul className="space-y-2">
|
||||
{(headcount.data ?? []).slice(0, 10).map((r) => (
|
||||
{headcountList.slice(0, 10).map((r) => (
|
||||
<li key={String(r.id)} className="rounded-md border p-3 text-sm">
|
||||
<div className="font-medium">{r.role_title} × {r.quantity} ({r.employee_type})</div>
|
||||
<div className="text-xs text-muted-foreground">dept #{r.department_id} · status: {r.status}</div>
|
||||
</li>
|
||||
))}
|
||||
{(headcount.data ?? []).length === 0 ? (
|
||||
{headcountList.length === 0 ? (
|
||||
<li className="text-sm text-muted-foreground">None yet.</li>
|
||||
) : null}
|
||||
</ul>
|
||||
@@ -228,13 +226,13 @@ export default function HRPage() {
|
||||
<div>
|
||||
<div className="mb-2 text-sm font-medium">Employment actions</div>
|
||||
<ul className="space-y-2">
|
||||
{(actions.data ?? []).slice(0, 10).map((a) => (
|
||||
{actionList.slice(0, 10).map((a) => (
|
||||
<li key={String(a.id)} className="rounded-md border p-3 text-sm">
|
||||
<div className="font-medium">{a.action_type} → employee #{a.employee_id}</div>
|
||||
<div className="text-xs text-muted-foreground">issued by #{a.issued_by_employee_id}</div>
|
||||
</li>
|
||||
))}
|
||||
{(actions.data ?? []).length === 0 ? (
|
||||
{actionList.length === 0 ? (
|
||||
<li className="text-sm text-muted-foreground">None yet.</li>
|
||||
) : null}
|
||||
</ul>
|
||||
@@ -258,7 +256,7 @@ export default function HRPage() {
|
||||
<Textarea placeholder="Tools/permissions (JSON or text)" value={onboardTools} onChange={(e) => setOnboardTools(e.target.value)} />
|
||||
<Select value={onboardOwnerId} onChange={(e) => setOnboardOwnerId(e.target.value)}>
|
||||
<option value="">Owner (HR)</option>
|
||||
{(employees.data ?? []).map((e) => (
|
||||
{employeeList.map((e) => (
|
||||
<option key={e.id ?? e.name} value={e.id ?? ""}>{e.name}</option>
|
||||
))}
|
||||
</Select>
|
||||
@@ -286,7 +284,7 @@ export default function HRPage() {
|
||||
<div>
|
||||
<div className="mb-2 text-sm font-medium">Current onboardings</div>
|
||||
<ul className="space-y-2">
|
||||
{(onboarding.data ?? []).map((o) => (
|
||||
{onboardingList.map((o) => (
|
||||
<li key={String(o.id)} className="rounded-md border p-3 text-sm">
|
||||
<div className="font-medium">{o.agent_name} · {o.role_title}</div>
|
||||
<div className="text-xs text-muted-foreground">status: {o.status} · cron: {o.cron_interval_ms ?? "—"}</div>
|
||||
@@ -327,7 +325,7 @@ export default function HRPage() {
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
{(onboarding.data ?? []).length === 0 ? (
|
||||
{onboardingList.length === 0 ? (
|
||||
<li className="text-sm text-muted-foreground">No onboarding records yet.</li>
|
||||
) : null}
|
||||
</ul>
|
||||
|
||||
@@ -6,6 +6,7 @@ import styles from "@/app/_components/Shell.module.css";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { normalizeActivities, normalizeDepartments, normalizeEmployees, normalizeProjects } from "@/lib/normalize";
|
||||
import { Select } from "@/components/ui/select";
|
||||
|
||||
import { useCreateProjectProjectsPost, useListProjectsProjectsGet } from "@/api/generated/projects/projects";
|
||||
@@ -15,9 +16,13 @@ import { useListActivitiesActivitiesGet } from "@/api/generated/activities/activ
|
||||
|
||||
export default function Home() {
|
||||
const projects = useListProjectsProjectsGet();
|
||||
const projectList = normalizeProjects(projects.data);
|
||||
const departments = useListDepartmentsDepartmentsGet();
|
||||
const departmentList = normalizeDepartments(departments.data);
|
||||
const employees = useListEmployeesEmployeesGet();
|
||||
const activities = useListActivitiesActivitiesGet({ limit: 20 });
|
||||
const employeeList = normalizeEmployees(employees.data);
|
||||
const activityList = normalizeActivities(activities.data);
|
||||
|
||||
const [projectName, setProjectName] = useState("");
|
||||
const [deptName, setDeptName] = useState("");
|
||||
@@ -81,13 +86,13 @@ export default function Home() {
|
||||
<div className={styles.card}>
|
||||
<div className={styles.cardTitle}>Live activity</div>
|
||||
<div className={styles.list}>
|
||||
{(activities.data ?? []).map((a) => (
|
||||
{activityList.map((a) => (
|
||||
<div key={String(a.id)} className={styles.item}>
|
||||
<div style={{ fontWeight: 600 }}>{a.entity_type} · {a.verb}</div>
|
||||
<div className={styles.mono}>id {a.entity_id ?? "—"}</div>
|
||||
</div>
|
||||
))}
|
||||
{(activities.data ?? []).length === 0 ? (
|
||||
{activityList.length === 0 ? (
|
||||
<div className={styles.mono}>No activity yet.</div>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -98,17 +103,17 @@ export default function Home() {
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Projects</CardTitle>
|
||||
<CardDescription>{(projects.data ?? []).length} total</CardDescription>
|
||||
<CardDescription>{projectList.length} total</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className={styles.list}>
|
||||
{(projects.data ?? []).slice(0, 8).map((p) => (
|
||||
{projectList.slice(0, 8).map((p) => (
|
||||
<div key={p.id ?? p.name} className={styles.item}>
|
||||
<div style={{ fontWeight: 600 }}>{p.name}</div>
|
||||
<div className={styles.mono}>{p.status}</div>
|
||||
</div>
|
||||
))}
|
||||
{(projects.data ?? []).length === 0 ? <div className={styles.mono}>No projects yet.</div> : null}
|
||||
{projectList.length === 0 ? <div className={styles.mono}>No projects yet.</div> : null}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -116,17 +121,17 @@ export default function Home() {
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Departments</CardTitle>
|
||||
<CardDescription>{(departments.data ?? []).length} total</CardDescription>
|
||||
<CardDescription>{departmentList.length} total</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className={styles.list}>
|
||||
{(departments.data ?? []).slice(0, 8).map((d) => (
|
||||
{departmentList.slice(0, 8).map((d) => (
|
||||
<div key={d.id ?? d.name} className={styles.item}>
|
||||
<div style={{ fontWeight: 600 }}>{d.name}</div>
|
||||
<div className={styles.mono}>id {d.id}</div>
|
||||
</div>
|
||||
))}
|
||||
{(departments.data ?? []).length === 0 ? <div className={styles.mono}>No departments yet.</div> : null}
|
||||
{departmentList.length === 0 ? <div className={styles.mono}>No departments yet.</div> : null}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -134,17 +139,17 @@ export default function Home() {
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>People</CardTitle>
|
||||
<CardDescription>{(employees.data ?? []).length} total</CardDescription>
|
||||
<CardDescription>{employeeList.length} total</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className={styles.list}>
|
||||
{(employees.data ?? []).slice(0, 8).map((e) => (
|
||||
{employeeList.slice(0, 8).map((e) => (
|
||||
<div key={e.id ?? e.name} className={styles.item}>
|
||||
<div style={{ fontWeight: 600 }}>{e.name}</div>
|
||||
<div className={styles.mono}>{e.employee_type}</div>
|
||||
</div>
|
||||
))}
|
||||
{(employees.data ?? []).length === 0 ? <div className={styles.mono}>No people yet.</div> : null}
|
||||
{employeeList.length === 0 ? <div className={styles.mono}>No people yet.</div> : null}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { normalizeDepartments, normalizeEmployees } from "@/lib/normalize";
|
||||
import { Select } from "@/components/ui/select";
|
||||
|
||||
import {
|
||||
@@ -23,6 +24,8 @@ export default function PeoplePage() {
|
||||
|
||||
const employees = useListEmployeesEmployeesGet();
|
||||
const departments = useListDepartmentsDepartmentsGet();
|
||||
const departmentList = normalizeDepartments(departments.data);
|
||||
const employeeList = normalizeEmployees(employees.data);
|
||||
|
||||
const createEmployee = useCreateEmployeeEmployeesPost({
|
||||
mutation: {
|
||||
@@ -38,19 +41,19 @@ export default function PeoplePage() {
|
||||
|
||||
const deptNameById = useMemo(() => {
|
||||
const m = new Map<number, string>();
|
||||
for (const d of departments.data ?? []) {
|
||||
for (const d of departmentList) {
|
||||
if (d.id != null) m.set(d.id, d.name);
|
||||
}
|
||||
return m;
|
||||
}, [departments.data]);
|
||||
}, [departmentList]);
|
||||
|
||||
const empNameById = useMemo(() => {
|
||||
const m = new Map<number, string>();
|
||||
for (const e of employees.data ?? []) {
|
||||
for (const e of employeeList) {
|
||||
if (e.id != null) m.set(e.id, e.name);
|
||||
}
|
||||
return m;
|
||||
}, [employees.data]);
|
||||
}, [employeeList]);
|
||||
|
||||
return (
|
||||
<main className="mx-auto max-w-5xl p-6">
|
||||
@@ -79,7 +82,7 @@ export default function PeoplePage() {
|
||||
<Input placeholder="Title (optional)" value={title} onChange={(e) => setTitle(e.target.value)} />
|
||||
<Select value={departmentId} onChange={(e) => setDepartmentId(e.target.value)}>
|
||||
<option value="">(no department)</option>
|
||||
{(departments.data ?? []).map((d) => (
|
||||
{departmentList.map((d) => (
|
||||
<option key={d.id ?? d.name} value={d.id ?? ""}>
|
||||
{d.name}
|
||||
</option>
|
||||
@@ -87,7 +90,7 @@ export default function PeoplePage() {
|
||||
</Select>
|
||||
<Select value={managerId} onChange={(e) => setManagerId(e.target.value)}>
|
||||
<option value="">(no manager)</option>
|
||||
{(employees.data ?? []).map((e) => (
|
||||
{employeeList.map((e) => (
|
||||
<option key={e.id ?? e.name} value={e.id ?? ""}>
|
||||
{e.name}
|
||||
</option>
|
||||
@@ -119,7 +122,7 @@ export default function PeoplePage() {
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Directory</CardTitle>
|
||||
<CardDescription>{(employees.data ?? []).length} total</CardDescription>
|
||||
<CardDescription>{employeeList.length} total</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{employees.isLoading ? <div className="text-sm text-muted-foreground">Loading…</div> : null}
|
||||
@@ -128,7 +131,7 @@ export default function PeoplePage() {
|
||||
) : null}
|
||||
{!employees.isLoading && !employees.error ? (
|
||||
<ul className="space-y-2">
|
||||
{(employees.data ?? []).map((e) => (
|
||||
{employeeList.map((e) => (
|
||||
<li key={e.id ?? e.name} className="rounded-md border p-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="font-medium">{e.name}</div>
|
||||
@@ -143,7 +146,7 @@ export default function PeoplePage() {
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
{(employees.data ?? []).length === 0 ? (
|
||||
{employeeList.length === 0 ? (
|
||||
<li className="text-sm text-muted-foreground">No people yet.</li>
|
||||
) : null}
|
||||
</ul>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useParams } from "next/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { normalizeEmployees, normalizeProjectMembers, normalizeProjects, normalizeTaskComments, normalizeTasks } from "@/lib/normalize";
|
||||
import { Select } from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
|
||||
@@ -33,11 +34,14 @@ export default function ProjectDetailPage() {
|
||||
const projectId = Number(params?.id);
|
||||
|
||||
const projects = useListProjectsProjectsGet();
|
||||
const project = (projects.data ?? []).find((p) => p.id === projectId);
|
||||
const projectList = normalizeProjects(projects.data);
|
||||
const project = projectList.find((p) => p.id === projectId);
|
||||
|
||||
const employees = useListEmployeesEmployeesGet();
|
||||
const employeeList = normalizeEmployees(employees.data);
|
||||
|
||||
const members = useListProjectMembersProjectsProjectIdMembersGet(projectId);
|
||||
const memberList = normalizeProjectMembers(members.data);
|
||||
const addMember = useAddProjectMemberProjectsProjectIdMembersPost({
|
||||
mutation: { onSuccess: () => members.refetch() },
|
||||
});
|
||||
@@ -48,7 +52,8 @@ export default function ProjectDetailPage() {
|
||||
mutation: { onSuccess: () => members.refetch() },
|
||||
});
|
||||
|
||||
const tasks = useListTasksTasksGet({ projectId });
|
||||
const tasks = useListTasksTasksGet({ project_id: projectId });
|
||||
const taskList = normalizeTasks(tasks.data);
|
||||
const createTask = useCreateTaskTasksPost({
|
||||
mutation: { onSuccess: () => tasks.refetch() },
|
||||
});
|
||||
@@ -68,9 +73,10 @@ export default function ProjectDetailPage() {
|
||||
const [commentBody, setCommentBody] = useState("");
|
||||
|
||||
const comments = useListTaskCommentsTaskCommentsGet(
|
||||
{ taskId: commentTaskId ?? 0 },
|
||||
{ task_id: commentTaskId ?? 0 },
|
||||
{ query: { enabled: Boolean(commentTaskId) } },
|
||||
);
|
||||
const commentList = normalizeTaskComments(comments.data);
|
||||
const addComment = useCreateTaskCommentTaskCommentsPost({
|
||||
mutation: {
|
||||
onSuccess: () => {
|
||||
@@ -81,18 +87,19 @@ export default function ProjectDetailPage() {
|
||||
});
|
||||
|
||||
const tasksByStatus = (() => {
|
||||
const map = new Map<string, typeof tasks.data>();
|
||||
const map = new Map<string, typeof taskList>();
|
||||
for (const s of STATUSES) map.set(s, []);
|
||||
for (const t of tasks.data ?? []) {
|
||||
map.get(t.status)?.push(t);
|
||||
for (const t of taskList) {
|
||||
const status = t.status ?? "backlog";
|
||||
map.get(status)?.push(t);
|
||||
}
|
||||
return map;
|
||||
})();
|
||||
|
||||
const employeeName = (id: number | null | undefined) =>
|
||||
employees.data?.find((e) => e.id === id)?.name ?? "—";
|
||||
employeeList.find((e) => e.id === id)?.name ?? "—";
|
||||
|
||||
const projectMembers = members.data ?? [];
|
||||
const projectMembers = memberList;
|
||||
|
||||
return (
|
||||
<main className="mx-auto max-w-6xl p-6">
|
||||
@@ -118,13 +125,13 @@ export default function ProjectDetailPage() {
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Select value={assigneeId} onChange={(e) => setAssigneeId(e.target.value)}>
|
||||
<option value="">Assignee</option>
|
||||
{(employees.data ?? []).map((e) => (
|
||||
{employeeList.map((e) => (
|
||||
<option key={e.id ?? e.name} value={e.id ?? ""}>{e.name}</option>
|
||||
))}
|
||||
</Select>
|
||||
<Select value={reviewerId} onChange={(e) => setReviewerId(e.target.value)}>
|
||||
<option value="">Reviewer</option>
|
||||
{(employees.data ?? []).map((e) => (
|
||||
{employeeList.map((e) => (
|
||||
<option key={e.id ?? e.name} value={e.id ?? ""}>{e.name}</option>
|
||||
))}
|
||||
</Select>
|
||||
@@ -162,7 +169,7 @@ export default function ProjectDetailPage() {
|
||||
e.currentTarget.value = "";
|
||||
}}>
|
||||
<option value="">Add member…</option>
|
||||
{(employees.data ?? []).map((e) => (
|
||||
{employeeList.map((e) => (
|
||||
<option key={e.id ?? e.name} value={e.id ?? ""}>{e.name}</option>
|
||||
))}
|
||||
</Select>
|
||||
@@ -274,14 +281,14 @@ export default function ProjectDetailPage() {
|
||||
Add comment
|
||||
</Button>
|
||||
<ul className="space-y-2">
|
||||
{(comments.data ?? []).map((c) => (
|
||||
{commentList.map((c) => (
|
||||
<li key={String(c.id)} className="rounded-md border p-2 text-sm">
|
||||
<div className="font-medium">{employeeName(c.author_employee_id)}</div>
|
||||
<div className="text-xs text-muted-foreground">{new Date(c.created_at).toLocaleString()}</div>
|
||||
<div className="text-xs text-muted-foreground">{(c.created_at ? new Date(c.created_at).toLocaleString() : "—")}</div>
|
||||
<div className="mt-1">{c.body}</div>
|
||||
</li>
|
||||
))}
|
||||
{(comments.data ?? []).length === 0 ? (
|
||||
{commentList.length === 0 ? (
|
||||
<li className="text-sm text-muted-foreground">No comments yet.</li>
|
||||
) : null}
|
||||
</ul>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import styles from "@/app/_components/Shell.module.css";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { normalizeProjects } from "@/lib/normalize";
|
||||
|
||||
import {
|
||||
useCreateProjectProjectsPost,
|
||||
@@ -17,6 +18,7 @@ export default function ProjectsPage() {
|
||||
const [name, setName] = useState("");
|
||||
|
||||
const projects = useListProjectsProjectsGet();
|
||||
const projectList = normalizeProjects(projects.data);
|
||||
const createProject = useCreateProjectProjectsPost({
|
||||
mutation: {
|
||||
onSuccess: () => {
|
||||
@@ -26,9 +28,7 @@ export default function ProjectsPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const sorted = useMemo(() => {
|
||||
return (projects.data ?? []).slice().sort((a, b) => a.name.localeCompare(b.name));
|
||||
}, [projects.data]);
|
||||
const sorted = projectList.slice().sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
return (
|
||||
<main>
|
||||
|
||||
110
frontend/src/lib/normalize.ts
Normal file
110
frontend/src/lib/normalize.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import type { Department } from "@/api/generated/model/department";
|
||||
|
||||
// Local activity shape (not generated as a model)
|
||||
export type Activity = {
|
||||
id?: number;
|
||||
actor_employee_id?: number | null;
|
||||
entity_type?: string;
|
||||
entity_id?: number | null;
|
||||
verb?: string;
|
||||
payload?: unknown;
|
||||
created_at?: string;
|
||||
};
|
||||
import type { Employee } from "@/api/generated/model/employee";
|
||||
import type { AgentOnboarding } from "@/api/generated/model/agentOnboarding";
|
||||
import type { EmploymentAction } from "@/api/generated/model/employmentAction";
|
||||
import type { HeadcountRequest } from "@/api/generated/model/headcountRequest";
|
||||
import type { Project } from "@/api/generated/model/project";
|
||||
import type { Task } from "@/api/generated/model/task";
|
||||
import type { ProjectMember } from "@/api/generated/model/projectMember";
|
||||
import type { TaskComment } from "@/api/generated/model/taskComment";
|
||||
|
||||
export function normalizeEmployees(data: unknown): Employee[] {
|
||||
if (Array.isArray(data)) return data as Employee[];
|
||||
if (data && typeof data === "object" && "data" in data) {
|
||||
const maybe = (data as { data?: unknown }).data;
|
||||
if (Array.isArray(maybe)) return maybe as Employee[];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export function normalizeDepartments(data: unknown): Department[] {
|
||||
if (Array.isArray(data)) return data as Department[];
|
||||
if (data && typeof data === "object" && "data" in data) {
|
||||
const maybe = (data as { data?: unknown }).data;
|
||||
if (Array.isArray(maybe)) return maybe as Department[];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export function normalizeHeadcountRequests(data: unknown): HeadcountRequest[] {
|
||||
if (Array.isArray(data)) return data as HeadcountRequest[];
|
||||
if (data && typeof data === "object" && "data" in data) {
|
||||
const maybe = (data as { data?: unknown }).data;
|
||||
if (Array.isArray(maybe)) return maybe as HeadcountRequest[];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export function normalizeEmploymentActions(data: unknown): EmploymentAction[] {
|
||||
if (Array.isArray(data)) return data as EmploymentAction[];
|
||||
if (data && typeof data === "object" && "data" in data) {
|
||||
const maybe = (data as { data?: unknown }).data;
|
||||
if (Array.isArray(maybe)) return maybe as EmploymentAction[];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export function normalizeAgentOnboardings(data: unknown): AgentOnboarding[] {
|
||||
if (Array.isArray(data)) return data as AgentOnboarding[];
|
||||
if (data && typeof data === "object" && "data" in data) {
|
||||
const maybe = (data as { data?: unknown }).data;
|
||||
if (Array.isArray(maybe)) return maybe as AgentOnboarding[];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export function normalizeActivities(data: unknown): Activity[] {
|
||||
if (Array.isArray(data)) return data as Activity[];
|
||||
if (data && typeof data === "object" && "data" in data) {
|
||||
const maybe = (data as { data?: unknown }).data;
|
||||
if (Array.isArray(maybe)) return maybe as Activity[];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export function normalizeProjects(data: unknown): Project[] {
|
||||
if (Array.isArray(data)) return data as Project[];
|
||||
if (data && typeof data === "object" && "data" in data) {
|
||||
const maybe = (data as { data?: unknown }).data;
|
||||
if (Array.isArray(maybe)) return maybe as Project[];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export function normalizeTasks(data: unknown): Task[] {
|
||||
if (Array.isArray(data)) return data as Task[];
|
||||
if (data && typeof data === "object" && "data" in data) {
|
||||
const maybe = (data as { data?: unknown }).data;
|
||||
if (Array.isArray(maybe)) return maybe as Task[];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export function normalizeTaskComments(data: unknown): TaskComment[] {
|
||||
if (Array.isArray(data)) return data as TaskComment[];
|
||||
if (data && typeof data === "object" && "data" in data) {
|
||||
const maybe = (data as { data?: unknown }).data;
|
||||
if (Array.isArray(maybe)) return maybe as TaskComment[];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export function normalizeProjectMembers(data: unknown): ProjectMember[] {
|
||||
if (Array.isArray(data)) return data as ProjectMember[];
|
||||
if (data && typeof data === "object" && "data" in data) {
|
||||
const maybe = (data as { data?: unknown }).data;
|
||||
if (Array.isArray(maybe)) return maybe as ProjectMember[];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
Reference in New Issue
Block a user