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