Compare commits
10 Commits
018f23f833
...
8ce5d3e8dc
| Author | SHA1 | Date |
|---|---|---|
|
|
8ce5d3e8dc | |
|
|
8e88205bf0 | |
|
|
51769608d7 | |
|
|
55c2c10c51 | |
|
|
c44238b468 | |
|
|
2b52959531 | |
|
|
1aa9a37898 | |
|
|
fa3a9494bd | |
|
|
f10b38d020 | |
|
|
bd26a0446d |
|
|
@ -0,0 +1,13 @@
|
||||||
|
import DetailAgent from "@/components/agent/detail-agent";
|
||||||
|
import Sidebar from "@/components/sidebar";
|
||||||
|
|
||||||
|
export default function UserDetailPage() {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen">
|
||||||
|
<Sidebar />
|
||||||
|
<main className="flex-1 bg-gray-50 p-6">
|
||||||
|
<DetailAgent />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,15 +1,19 @@
|
||||||
// app/chat/page.tsx
|
// app/chat/page.tsx
|
||||||
|
"use client";
|
||||||
|
import AgentsManagement from "@/components/dashboard/admin-agenst";
|
||||||
import Agents from "@/components/dashboard/agents";
|
import Agents from "@/components/dashboard/agents";
|
||||||
import Chat from "@/components/dashboard/chat";
|
import Chat from "@/components/dashboard/chat";
|
||||||
import Sidebar from "@/components/sidebar";
|
import Sidebar from "@/components/sidebar";
|
||||||
|
import { getCookiesDecrypt } from "@/utils/globals";
|
||||||
|
|
||||||
export default function ChatPage() {
|
export default function ChatPage() {
|
||||||
|
const roleId = getCookiesDecrypt("urie");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen">
|
<div className="flex min-h-screen">
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
<main className="flex-1 bg-white p-2 lg:p-6">
|
<main className="flex-1 bg-white p-2 lg:p-6">
|
||||||
<Agents />
|
{Number(roleId) < 4 ? <AgentsManagement /> : <Agents />}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { createAgentData } from "@/service/agent";
|
||||||
|
|
||||||
|
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
|
||||||
|
export default function GetAgentsMapping() {
|
||||||
|
const getDatas = async () => {
|
||||||
|
const res = await fetch(
|
||||||
|
"https://narasiahli.com/ai/api/v1/agents/?skip=0&limit=100&include_teams=true",
|
||||||
|
);
|
||||||
|
const json = await res.json();
|
||||||
|
|
||||||
|
for (const element of json) {
|
||||||
|
const data = {
|
||||||
|
agent_id: element.id,
|
||||||
|
description: element.description,
|
||||||
|
instructions: element.instruction ?? "",
|
||||||
|
is_active: true,
|
||||||
|
name: element.name,
|
||||||
|
status: true,
|
||||||
|
type: "tenaga ahli",
|
||||||
|
};
|
||||||
|
|
||||||
|
await createAgentData(data);
|
||||||
|
|
||||||
|
// ⏳ jeda 5 detik
|
||||||
|
await sleep(5000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Button onClick={getDatas}>GetData</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,262 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import Navbar from "@/components/navbar";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { z } from "zod";
|
||||||
|
import Swal from "sweetalert2";
|
||||||
|
import withReactContent from "sweetalert2-react-content";
|
||||||
|
import { useParams, useRouter } from "next/navigation";
|
||||||
|
import { close, error, loading } from "@/config/swal";
|
||||||
|
import { getAgentById, updateAgentById } from "@/service/agent";
|
||||||
|
import { Textarea } from "../ui/textarea";
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
name: z.string().min(2, {
|
||||||
|
message: "Nama wajib diisi",
|
||||||
|
}),
|
||||||
|
description: z.string().min(2, {
|
||||||
|
message: "Deskripsi wajib diisi",
|
||||||
|
}),
|
||||||
|
instructions: z.string().min(2, {
|
||||||
|
message: "Instruksi wajib diisi",
|
||||||
|
}),
|
||||||
|
type: z.string().min(2, {
|
||||||
|
message: "Keahlian wajib diisi",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormData = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
interface AgentData {
|
||||||
|
id: number;
|
||||||
|
fullname: string;
|
||||||
|
email: string;
|
||||||
|
whatsappNumber: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DetailAgent() {
|
||||||
|
const param = useParams();
|
||||||
|
const id = param.id;
|
||||||
|
const [isEdit, setIsEdit] = useState(false);
|
||||||
|
const MySwal = withReactContent(Swal);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
setValue,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm<FormData>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
type: "",
|
||||||
|
instructions: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initFetch();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const initFetch = async () => {
|
||||||
|
loading();
|
||||||
|
// const req = { page: page, title: search, limit: limit, createdById: "" };
|
||||||
|
const res = await getAgentById(id as string);
|
||||||
|
// setTotalData(res?.meta?.count || 0);
|
||||||
|
const data = res?.data?.data;
|
||||||
|
setValue("name", data?.name);
|
||||||
|
setValue("description", data?.description);
|
||||||
|
setValue("type", data?.type);
|
||||||
|
setValue("instructions", data?.instructions);
|
||||||
|
close();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async (data: FormData) => {
|
||||||
|
MySwal.fire({
|
||||||
|
title: "Simpan Data",
|
||||||
|
text: "",
|
||||||
|
icon: "warning",
|
||||||
|
showCancelButton: true,
|
||||||
|
cancelButtonColor: "#d33",
|
||||||
|
confirmButtonColor: "#3085d6",
|
||||||
|
confirmButtonText: "Simpan",
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
save(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const save = async (data: FormData) => {
|
||||||
|
const { name, instructions, type, description } = data;
|
||||||
|
loading();
|
||||||
|
const request = {
|
||||||
|
name,
|
||||||
|
instructions,
|
||||||
|
description,
|
||||||
|
type: type.toLowerCase(),
|
||||||
|
isActive: true,
|
||||||
|
status: true,
|
||||||
|
};
|
||||||
|
const res = await updateAgentById(request, id as string);
|
||||||
|
if (res?.error) {
|
||||||
|
error(res?.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
close();
|
||||||
|
setIsEdit(false);
|
||||||
|
successSubmit();
|
||||||
|
};
|
||||||
|
|
||||||
|
function successSubmit() {
|
||||||
|
MySwal.fire({
|
||||||
|
title: "Sukses",
|
||||||
|
icon: "success",
|
||||||
|
confirmButtonColor: "#3085d6",
|
||||||
|
confirmButtonText: "OK",
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
initFetch();
|
||||||
|
} else {
|
||||||
|
initFetch();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{" "}
|
||||||
|
<Navbar
|
||||||
|
title="Tenaga Ahli,/admin/agents/"
|
||||||
|
subTitle={isEdit ? "Edit" : "Detail"}
|
||||||
|
/>
|
||||||
|
<form
|
||||||
|
className="flex flex-col gap-5 mt-10 mb-5"
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
>
|
||||||
|
<div className="flex flex-row gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={() => setIsEdit(true)}
|
||||||
|
type="button"
|
||||||
|
className={`cursor-pointer w-fit `}
|
||||||
|
disabled={isEdit}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
{isEdit && (
|
||||||
|
<Button type="submit" className="cursor-pointer w-fit bg-green-500">
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<div className="relative">
|
||||||
|
<label
|
||||||
|
htmlFor="name"
|
||||||
|
className="absolute -top-2 left-3 bg-gray-50 px-1 text-xs text-muted-foreground rounded-2xl"
|
||||||
|
>
|
||||||
|
Nama Lengkap
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
id="name"
|
||||||
|
placeholder="Masukkan Nama Lengkap"
|
||||||
|
className="h-12 rounded-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none"
|
||||||
|
readOnly={!isEdit}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{errors.name && (
|
||||||
|
<p className="text-red-500 text-sm">{errors.name.message}</p>
|
||||||
|
)}
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="instructions"
|
||||||
|
render={({ field }) => (
|
||||||
|
<div className="relative">
|
||||||
|
<label
|
||||||
|
htmlFor="instructions"
|
||||||
|
className="absolute -top-2 left-3 bg-gray-50 px-1 text-xs text-muted-foreground"
|
||||||
|
>
|
||||||
|
Instruksi
|
||||||
|
</label>
|
||||||
|
<Textarea
|
||||||
|
{...field}
|
||||||
|
id="instructions"
|
||||||
|
placeholder="Masukkan Instruksi"
|
||||||
|
className="h-36 rounded-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none"
|
||||||
|
readOnly={!isEdit}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{errors.instructions && (
|
||||||
|
<p className="text-red-500 text-sm">{errors.instructions.message}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="description"
|
||||||
|
render={({ field }) => (
|
||||||
|
<div className="relative">
|
||||||
|
<label
|
||||||
|
htmlFor="description"
|
||||||
|
className="absolute -top-2 left-3 bg-gray-50 px-1 text-xs text-muted-foreground"
|
||||||
|
>
|
||||||
|
Instruksi
|
||||||
|
</label>
|
||||||
|
<Textarea
|
||||||
|
{...field}
|
||||||
|
id="description"
|
||||||
|
placeholder="Masukkan Instruksi"
|
||||||
|
className="h-36 rounded-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none"
|
||||||
|
readOnly={!isEdit}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{errors.description && (
|
||||||
|
<p className="text-red-500 text-sm">{errors.description.message}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="type"
|
||||||
|
render={({ field }) => (
|
||||||
|
<div className="relative">
|
||||||
|
<label
|
||||||
|
htmlFor="type"
|
||||||
|
className="absolute -top-2 left-3 bg-gray-50 px-1 text-xs text-muted-foreground rounded-2xl"
|
||||||
|
>
|
||||||
|
Nama Lengkap
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
id="type"
|
||||||
|
placeholder="Masukkan Nama Lengkap"
|
||||||
|
className="h-12 rounded-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none"
|
||||||
|
readOnly={!isEdit}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{errors.type && (
|
||||||
|
<p className="text-red-500 text-sm">{errors.type.message}</p>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
<Button color="destructive" onClick={router.back}>
|
||||||
|
Kembali
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,266 @@
|
||||||
|
"use client";
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import Navbar from "../navbar";
|
||||||
|
import { close, loading } from "@/config/swal";
|
||||||
|
import {
|
||||||
|
createAgentData,
|
||||||
|
getAgentByAgentId,
|
||||||
|
getAllAgent,
|
||||||
|
} from "@/service/agent";
|
||||||
|
import { convertDateFormat, getCookiesDecrypt } from "@/utils/globals";
|
||||||
|
import Link from "next/link";
|
||||||
|
import CustomPagination from "../custom-pagination";
|
||||||
|
import { Switch } from "../ui/switch";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "../ui/dialog";
|
||||||
|
import { Input } from "../ui/input";
|
||||||
|
|
||||||
|
interface AgentDetail {
|
||||||
|
agent_id: string;
|
||||||
|
description: string;
|
||||||
|
instructions: string;
|
||||||
|
is_active: boolean;
|
||||||
|
name: string;
|
||||||
|
status: boolean;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
|
||||||
|
export default function AgentsManagement() {
|
||||||
|
const roleId = getCookiesDecrypt("urie");
|
||||||
|
const ulne = getCookiesDecrypt("ulne");
|
||||||
|
|
||||||
|
const [agents, setAgents] = useState<any>([]);
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
const [activeCategory, setActiveCategory] = useState("Semua");
|
||||||
|
const [currentPage, setCurrentPage] = useState(0);
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [limit, setLimit] = useState(5);
|
||||||
|
const [totalData, setTotalData] = useState(0);
|
||||||
|
const [agentDetail, setAgentDetail] = useState<AgentDetail[]>([]);
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const ITEMS_PER_PAGE = 6;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initFetch();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const initFetch = async () => {
|
||||||
|
loading();
|
||||||
|
// const req = { page: page, title: search, limit: limit, createdById: "" };
|
||||||
|
const res = await getAllAgent();
|
||||||
|
// setTotalData(res?.meta?.count || 0);
|
||||||
|
setAgents(res?.data?.data || []);
|
||||||
|
close();
|
||||||
|
};
|
||||||
|
|
||||||
|
// filter berdasarkan kategori & search
|
||||||
|
const filteredagents = agents.filter((agent: any) => {
|
||||||
|
const matchesCategory =
|
||||||
|
activeCategory === "Semua" || agent.role?.includes(activeCategory);
|
||||||
|
const matchesSearch = agent.name
|
||||||
|
?.toLowerCase()
|
||||||
|
.includes(searchTerm.toLowerCase());
|
||||||
|
return matchesCategory && matchesSearch;
|
||||||
|
});
|
||||||
|
|
||||||
|
const items = useMemo(() => {
|
||||||
|
const start = (page - 1) * limit;
|
||||||
|
const end = start + limit;
|
||||||
|
|
||||||
|
return filteredagents.slice(start, end);
|
||||||
|
}, [page, filteredagents, limit]);
|
||||||
|
|
||||||
|
const totalPages = Math.ceil(filteredagents.length / limit);
|
||||||
|
|
||||||
|
const getDataAgent = async () => {
|
||||||
|
loading();
|
||||||
|
const res = await fetch(
|
||||||
|
"https://narasiahli.com/ai/api/v1/agents/?skip=0&limit=100&include_teams=true",
|
||||||
|
);
|
||||||
|
const json = await res.json();
|
||||||
|
const temp = [];
|
||||||
|
for (const element of json) {
|
||||||
|
const data = {
|
||||||
|
agent_id: element.id,
|
||||||
|
description: element.description,
|
||||||
|
instructions: element.instruction ?? "",
|
||||||
|
is_active: true,
|
||||||
|
name: element.name,
|
||||||
|
status: true,
|
||||||
|
type: "",
|
||||||
|
};
|
||||||
|
const now = await checkAgent(element.id);
|
||||||
|
if (now) {
|
||||||
|
temp.push(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setAgentDetail(temp);
|
||||||
|
close();
|
||||||
|
setIsModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkAgent = async (id: string) => {
|
||||||
|
const res = await getAgentByAgentId(id);
|
||||||
|
return res.error ? true : false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExpertiseChange = (index: number, value: string) => {
|
||||||
|
setAgentDetail((prev) => {
|
||||||
|
const updated = [...prev];
|
||||||
|
updated[index] = {
|
||||||
|
...updated[index],
|
||||||
|
type: value,
|
||||||
|
};
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
loading();
|
||||||
|
for (const element of agentDetail) {
|
||||||
|
const data = {
|
||||||
|
agent_id: element.agent_id,
|
||||||
|
description: element.description,
|
||||||
|
instructions: element.instructions ?? "",
|
||||||
|
is_active: true,
|
||||||
|
name: element.name,
|
||||||
|
status: true,
|
||||||
|
type: element.type.toLocaleLowerCase(),
|
||||||
|
};
|
||||||
|
await createAgentData(data);
|
||||||
|
|
||||||
|
await sleep(5000);
|
||||||
|
}
|
||||||
|
close();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-1 overflow-hidden flex flex-col h-[90vh]">
|
||||||
|
<Navbar title="Tenaga Ahli" />
|
||||||
|
<Button onClick={getDataAgent} className="mt-10 w-fit">
|
||||||
|
Perbarui Data
|
||||||
|
</Button>
|
||||||
|
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
|
||||||
|
{/* <DialogTrigger asChild>
|
||||||
|
<Button variant="outline">Scrollable Content</Button>
|
||||||
|
</DialogTrigger> */}
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Perbarui Data Tenaga Ahli</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="no-scrollbar -mx-4 max-h-[50vh] overflow-y-auto px-4">
|
||||||
|
{agentDetail.map((agent, index) => (
|
||||||
|
<div key={agent.agent_id} className="mb-4 flex flex-col gap-1">
|
||||||
|
<p className="text-sm">
|
||||||
|
Nama : <span className="font-semibold">{agent.name}</span>
|
||||||
|
</p>
|
||||||
|
<p className="text-sm">Deskripsi : {agent.description}</p>
|
||||||
|
<p className="text-sm">
|
||||||
|
Instruksi :{" "}
|
||||||
|
{agent.instructions == "" ? "-" : agent.instructions}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm">Keahlian :</p>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
placeholder="Contoh : Ahli Hukum"
|
||||||
|
className="h-12 rounded-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none"
|
||||||
|
value={agent.type}
|
||||||
|
onChange={(e) => handleExpertiseChange(index, e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button onClick={handleSave}>Save</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<div className="border rounded-md overflow-auto mt-3">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-gray-50 text-left">
|
||||||
|
{/* <th className="px-4 py-2 font-medium">Title knowledge</th> */}
|
||||||
|
<th className="px-4 py-2 font-medium">Agent Name</th>
|
||||||
|
<th className="px-4 py-2 font-medium">Agent Type</th>
|
||||||
|
<th className="px-4 py-2 font-medium">Agent Description</th>
|
||||||
|
<th className="px-4 py-2 font-medium">Aksi</th>
|
||||||
|
{/* <th className="px-4 py-2 font-medium">Tindakan</th> */}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{items.map((item: any) => (
|
||||||
|
<tr key={item.id} className="border-t">
|
||||||
|
{/* <td className="px-4 py-2">{item.title}</td> */}
|
||||||
|
<td className="px-4 py-2">{item.name}</td>
|
||||||
|
<td className="px-4 py-2 uppercase">{item.type}</td>
|
||||||
|
<td className="px-4 py-2">{item.description}</td>
|
||||||
|
{/* <td className="px-4 py-2">
|
||||||
|
{convertDateFormat(item.createdAt)}
|
||||||
|
</td> */}
|
||||||
|
<td className="px-4 py-2 space-x-3">
|
||||||
|
<Link
|
||||||
|
href={`/admin/agents/detail/${item.id}`}
|
||||||
|
className="text-blue-500 hover:underline"
|
||||||
|
>
|
||||||
|
Detail
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<button className="text-red-500 hover:underline">
|
||||||
|
Hapus
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{/* <td className="px-4 py-2 flex flex-row gap-2">
|
||||||
|
<Link
|
||||||
|
href={`/admin/data-knowledge/detail/${item.id}`}
|
||||||
|
className="text-blue-600 cursor-pointer hover:underline"
|
||||||
|
>
|
||||||
|
View
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{(Number(uie) === item.createdById || ulne == "2") && (
|
||||||
|
<a
|
||||||
|
onClick={() => handleDeleteKnowledgeBase(item.id)}
|
||||||
|
className="text-red-600 cursor-pointer hover:underline"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</td> */}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-end gap-2 px-4 py-2 text-sm text-gray-600 border-t">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
Rows per page:
|
||||||
|
<select
|
||||||
|
className="border rounded-md px-2 py-1 text-sm"
|
||||||
|
value={limit}
|
||||||
|
onChange={(e) => setLimit(Number(e.target.value))}
|
||||||
|
>
|
||||||
|
<option value={5}>5</option>
|
||||||
|
<option value={10}>10</option>
|
||||||
|
<option value={20}>20</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CustomPagination totalPage={totalPages} onPageChange={setPage} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,9 @@ import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { BellIcon, ChevronLeft, ChevronRight, Search } from "lucide-react";
|
import { BellIcon, ChevronLeft, ChevronRight, Search } from "lucide-react";
|
||||||
|
import { getAllExperts } from "@/service/user";
|
||||||
|
import Navbar from "../navbar";
|
||||||
|
import { close, loading } from "@/config/swal";
|
||||||
|
|
||||||
const categories = [
|
const categories = [
|
||||||
"Semua",
|
"Semua",
|
||||||
|
|
@ -26,68 +29,65 @@ export default function Agents() {
|
||||||
const [activeCategory, setActiveCategory] = useState("Semua");
|
const [activeCategory, setActiveCategory] = useState("Semua");
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
const [totalPages, setTotalPages] = useState(1);
|
||||||
|
|
||||||
const toggleSelect = (id: number) => {
|
const toggleSelect = (id: number) => {
|
||||||
setSelected((prev) =>
|
setSelected((prev) =>
|
||||||
prev.includes(id) ? prev.filter((s) => s !== id) : [...prev, id]
|
prev.includes(id) ? prev.filter((s) => s !== id) : [...prev, id],
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchExperts = async () => {
|
|
||||||
try {
|
|
||||||
const res = await fetch(
|
|
||||||
"https://narasiahli.com/ai/api/v1/agents/?skip=0&limit=15&include_teams=true"
|
|
||||||
);
|
|
||||||
const json = await res.json();
|
|
||||||
setExperts(json || []);
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Gagal mengambil data tenaga ahli:", err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchExperts();
|
fetchExperts();
|
||||||
}, []);
|
}, [activeCategory]);
|
||||||
|
|
||||||
|
const fetchExperts = async () => {
|
||||||
|
// try {
|
||||||
|
// const res = await fetch(
|
||||||
|
// "https://narasiahli.com/ai/api/v1/agents/?skip=0&limit=15&include_teams=true"
|
||||||
|
// );
|
||||||
|
// const json = await res.json();
|
||||||
|
// setExperts(json || []);
|
||||||
|
// } catch (err) {
|
||||||
|
// console.error("Gagal mengambil data tenaga ahli:", err);
|
||||||
|
// }
|
||||||
|
loading();
|
||||||
|
const res = await getAllExperts(
|
||||||
|
search,
|
||||||
|
activeCategory == "Semua" ? "" : activeCategory.toLocaleLowerCase(),
|
||||||
|
);
|
||||||
|
|
||||||
|
setExperts(res?.data?.data ?? []);
|
||||||
|
console.log("ress", res);
|
||||||
|
close();
|
||||||
|
};
|
||||||
|
|
||||||
// filter berdasarkan kategori & search
|
// filter berdasarkan kategori & search
|
||||||
const filteredExperts = experts.filter((expert) => {
|
// const filteredExperts = experts.filter((expert) => {
|
||||||
const matchesCategory =
|
// const matchesCategory =
|
||||||
activeCategory === "Semua" || expert.role?.includes(activeCategory);
|
// activeCategory === "Semua" || expert.role?.includes(activeCategory);
|
||||||
const matchesSearch = expert.name
|
// const matchesSearch = expert.name
|
||||||
?.toLowerCase()
|
// ?.toLowerCase()
|
||||||
.includes(searchTerm.toLowerCase());
|
// .includes(searchTerm.toLowerCase());
|
||||||
return matchesCategory && matchesSearch;
|
// return matchesCategory && matchesSearch;
|
||||||
});
|
// });
|
||||||
|
|
||||||
const paginatedExperts = filteredExperts.slice(
|
// const paginatedExperts = filteredExperts.slice(
|
||||||
currentPage * ITEMS_PER_PAGE,
|
// currentPage * ITEMS_PER_PAGE,
|
||||||
(currentPage + 1) * ITEMS_PER_PAGE
|
// (currentPage + 1) * ITEMS_PER_PAGE,
|
||||||
);
|
// );
|
||||||
|
|
||||||
const totalPages = Math.ceil(filteredExperts.length / ITEMS_PER_PAGE);
|
// const totalPages = Math.ceil(filteredExperts.length / ITEMS_PER_PAGE);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-7xl mx-auto ">
|
<div className="flex-1 overflow-hidden flex flex-col h-[90vh]">
|
||||||
<div className="flex items-center justify-between">
|
<Navbar title="Tenaga Ahli" />
|
||||||
<h1 className="text-lg font-semibold">Profile Tenaga Ahli</h1>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<Input
|
|
||||||
placeholder="Search"
|
|
||||||
value={search}
|
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
|
||||||
className="w-[200px] md:w-[250px]"
|
|
||||||
/>
|
|
||||||
<button className="relative p-2 rounded-full hover:bg-gray-100">
|
|
||||||
<BellIcon className="w-5 h-5 text-gray-500" />
|
|
||||||
{/* Notifikasi badge bisa ditambahkan di sini */}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap items-center gap-2 mb-4 pt-3">
|
<div className="flex flex-wrap items-center gap-2 mb-4 pt-3">
|
||||||
{categories.map((cat) => (
|
{categories.map((cat) => (
|
||||||
<Button
|
<Button
|
||||||
key={cat}
|
key={cat}
|
||||||
size="sm"
|
size="sm"
|
||||||
|
className="cursor-pointer"
|
||||||
variant={activeCategory === cat ? "default" : "outline"}
|
variant={activeCategory === cat ? "default" : "outline"}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setActiveCategory(cat);
|
setActiveCategory(cat);
|
||||||
|
|
@ -113,7 +113,7 @@ export default function Agents() {
|
||||||
|
|
||||||
{/* List Expert */}
|
{/* List Expert */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-6">
|
||||||
{paginatedExperts.map((expert) => (
|
{experts.map((expert) => (
|
||||||
<div
|
<div
|
||||||
key={expert.id}
|
key={expert.id}
|
||||||
className="border rounded-lg p-4 bg-white shadow-sm"
|
className="border rounded-lg p-4 bg-white shadow-sm"
|
||||||
|
|
@ -124,12 +124,12 @@ export default function Agents() {
|
||||||
onCheckedChange={() => toggleSelect(expert.id)}
|
onCheckedChange={() => toggleSelect(expert.id)}
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
src={expert.image_url || "/default.png"}
|
src={"/profile.png"}
|
||||||
alt={expert.name}
|
alt={expert.name}
|
||||||
className="w-20 h-24 object-cover rounded"
|
className="w-10 h-10 object-cover rounded"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-semibold text-sm">{expert.name}</h3>
|
<h3 className="font-semibold text-sm">{expert.fullname}</h3>
|
||||||
<p className="text-xs text-gray-500">{expert.role}</p>
|
<p className="text-xs text-gray-500">{expert.role}</p>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|
@ -152,10 +152,10 @@ export default function Agents() {
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{expert.articles?.map((art: any, idx: number) => (
|
{expert.researchJournals?.map((art: any, idx: number) => (
|
||||||
<tr key={idx} className="border-b">
|
<tr key={idx} className="border-b">
|
||||||
<td className="py-1 px-2">{art.title}</td>
|
<td className="py-1 px-2">{art.journalTitle}</td>
|
||||||
<td className="py-1 px-2">{art.publish}</td>
|
<td className="py-1 px-2">{art.publisher}</td>
|
||||||
<td className="py-1 px-2 text-blue-600 cursor-pointer">
|
<td className="py-1 px-2 text-blue-600 cursor-pointer">
|
||||||
Lihat
|
Lihat
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -180,7 +180,7 @@ export default function Agents() {
|
||||||
<ChevronLeft className="h-4 w-4" />
|
<ChevronLeft className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{Array.from({ length: totalPages }).map((_, i) => (
|
{/* {Array.from({ length: totalPages }).map((_, i) => (
|
||||||
<Button
|
<Button
|
||||||
key={i}
|
key={i}
|
||||||
variant={currentPage === i ? "default" : "ghost"}
|
variant={currentPage === i ? "default" : "ghost"}
|
||||||
|
|
@ -190,7 +190,7 @@ export default function Agents() {
|
||||||
>
|
>
|
||||||
{i + 1}
|
{i + 1}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))} */}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ import FileMapper from "../main/chat-ai/file-mapper";
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import OpusMediaRecorder from "opus-media-recorder";
|
import OpusMediaRecorder from "opus-media-recorder";
|
||||||
|
import { error } from "@/config/swal";
|
||||||
|
|
||||||
OpusMediaRecorder.config = {
|
OpusMediaRecorder.config = {
|
||||||
workerOptions: {
|
workerOptions: {
|
||||||
|
|
@ -123,7 +124,7 @@ export default function Chat() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`https://narasiahli.com/ai/api/v1/agents/${agentId}`
|
`https://narasiahli.com/ai/api/v1/agents/${agentId}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|
@ -168,7 +169,7 @@ export default function Chat() {
|
||||||
const fetchExperts = async () => {
|
const fetchExperts = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
"https://narasiahli.com/ai/api/v1/agents/?skip=0&limit=100&include_teams=true"
|
"https://narasiahli.com/ai/api/v1/agents/?skip=0&limit=100&include_teams=true",
|
||||||
);
|
);
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
// console.log("DATA EXPERTS:", json);
|
// console.log("DATA EXPERTS:", json);
|
||||||
|
|
@ -181,7 +182,7 @@ export default function Chat() {
|
||||||
a.name.includes("Kementerian") ||
|
a.name.includes("Kementerian") ||
|
||||||
a.name.includes("Polri") ||
|
a.name.includes("Polri") ||
|
||||||
a.name.includes("MediaHub") ||
|
a.name.includes("MediaHub") ||
|
||||||
a.name.includes("KUHP")
|
a.name.includes("KUHP"),
|
||||||
);
|
);
|
||||||
setExperts(filtered || []);
|
setExperts(filtered || []);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -295,7 +296,7 @@ export default function Chat() {
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: formData,
|
body: formData,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
@ -351,9 +352,13 @@ export default function Chat() {
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: formData,
|
body: formData,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
if (!res) {
|
||||||
|
error("Gagal Mengirim Pesan");
|
||||||
|
setLoading(false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setIsSent(true);
|
setIsSent(true);
|
||||||
setQuestion(data.query || currentInput);
|
setQuestion(data.query || currentInput);
|
||||||
|
|
@ -484,7 +489,7 @@ export default function Chat() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredExperts = experts.filter((expert) =>
|
const filteredExperts = experts.filter((expert) =>
|
||||||
expert.name?.toLowerCase().includes(search.toLowerCase())
|
expert.name?.toLowerCase().includes(search.toLowerCase()),
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleTaggedExpert = () => {
|
const handleTaggedExpert = () => {
|
||||||
|
|
@ -639,7 +644,7 @@ export default function Chat() {
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
const filter = multipleAgent.filter(
|
const filter = multipleAgent.filter(
|
||||||
(a) => a !== expert.id
|
(a) => a !== expert.id,
|
||||||
);
|
);
|
||||||
setMultipleAgent(filter);
|
setMultipleAgent(filter);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,139 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { BellIcon, Eye } from "lucide-react";
|
import { BellIcon, Eye } from "lucide-react";
|
||||||
|
import Navbar from "../navbar";
|
||||||
|
import { convertDateFormatNoTimeV2, getCookiesDecrypt } from "@/utils/globals";
|
||||||
|
import {
|
||||||
|
getUserDetail,
|
||||||
|
getUserEducationHistory,
|
||||||
|
getUserResearchJournal,
|
||||||
|
getUserWorkHistory,
|
||||||
|
} from "@/service/user";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import { Input } from "../ui/input";
|
||||||
|
|
||||||
|
interface WorkHistory {
|
||||||
|
id: number;
|
||||||
|
userId: number;
|
||||||
|
jobTitle: string;
|
||||||
|
companyName: string;
|
||||||
|
startDate: Date;
|
||||||
|
endDate: Date;
|
||||||
|
}
|
||||||
|
interface EducationHistory {
|
||||||
|
id: number;
|
||||||
|
userId: number;
|
||||||
|
schoolName: string;
|
||||||
|
educationLevel: string;
|
||||||
|
certificateImage: string;
|
||||||
|
major: string;
|
||||||
|
graduationYear: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ResearchJournals {
|
||||||
|
id: number;
|
||||||
|
userId: number;
|
||||||
|
journalTitle: string;
|
||||||
|
publisher: string;
|
||||||
|
journalUrl: string;
|
||||||
|
publishedDate: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Profile {
|
||||||
|
id: number;
|
||||||
|
fullname: string;
|
||||||
|
email: string;
|
||||||
|
phoneNumber: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default function Profile() {
|
export default function Profile() {
|
||||||
|
const uid = getCookiesDecrypt("uie");
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
const [search, setSearch] = useState("");
|
const [workHistory, setWorkHistory] = useState<WorkHistory[]>([]);
|
||||||
|
const [educationHistory, setEducationHistory] = useState<EducationHistory[]>(
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
const [researchJournals, setResearchJournal] = useState<ResearchJournals[]>(
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
const [profile, setProfile] = useState<Profile | null>(null);
|
||||||
|
const [isEdit, setIsEdit] = useState(false);
|
||||||
|
const [password, setPassword] = useState<string>("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getWorkHistory();
|
||||||
|
getEducationHistory();
|
||||||
|
getResearchJournal();
|
||||||
|
getDetail();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getWorkHistory = async () => {
|
||||||
|
const res = await getUserWorkHistory(Number(uid));
|
||||||
|
setWorkHistory(res?.data?.data);
|
||||||
|
};
|
||||||
|
const getEducationHistory = async () => {
|
||||||
|
const res = await getUserEducationHistory(Number(uid));
|
||||||
|
setEducationHistory(res?.data?.data);
|
||||||
|
};
|
||||||
|
const getResearchJournal = async () => {
|
||||||
|
const res = await getUserResearchJournal(Number(uid));
|
||||||
|
setResearchJournal(res?.data?.data);
|
||||||
|
};
|
||||||
|
const getDetail = async () => {
|
||||||
|
const res = await getUserDetail(Number(uid));
|
||||||
|
setProfile(res?.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleProfileChange = (key: keyof Profile, value: string) => {
|
||||||
|
if (!profile) return;
|
||||||
|
|
||||||
|
setProfile({
|
||||||
|
...profile,
|
||||||
|
[key]: value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEducationChange = (
|
||||||
|
index: number,
|
||||||
|
key: keyof EducationHistory,
|
||||||
|
value: string,
|
||||||
|
) => {
|
||||||
|
const updated = [...educationHistory];
|
||||||
|
updated[index] = {
|
||||||
|
...updated[index],
|
||||||
|
[key]: key === "graduationYear" ? Number(value) : value,
|
||||||
|
};
|
||||||
|
setEducationHistory(updated);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleWorkChange = (
|
||||||
|
index: number,
|
||||||
|
key: keyof WorkHistory,
|
||||||
|
value: string,
|
||||||
|
) => {
|
||||||
|
const updated = [...workHistory];
|
||||||
|
updated[index] = {
|
||||||
|
...updated[index],
|
||||||
|
[key]: value,
|
||||||
|
};
|
||||||
|
setWorkHistory(updated);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
// TODO: panggil API update profile
|
||||||
|
// await updateUserProfile(profile, password)
|
||||||
|
console.log("profile", profile);
|
||||||
|
|
||||||
|
setIsEdit(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-7xl mx-auto">
|
<div className="w-full space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<Navbar title="Profile" />
|
||||||
<h1 className="text-lg font-semibold">Profile</h1>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
{/* <Input
|
|
||||||
// placeholder="Search"
|
|
||||||
value={search}
|
|
||||||
// onChange={(e) => setSearch(e.target.value)}
|
|
||||||
// className="w-[200px] md:w-[250px]"
|
|
||||||
/> */}
|
|
||||||
<button className="relative p-2 rounded-full hover:bg-gray-100">
|
|
||||||
<BellIcon className="w-5 h-5 text-gray-500" />
|
|
||||||
{/* Notifikasi badge bisa ditambahkan di sini */}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<div className="w-16 h-16 rounded-full overflow-hidden">
|
<div className="w-16 h-16 rounded-full overflow-hidden">
|
||||||
|
|
@ -37,39 +146,47 @@ export default function Profile() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button className="bg-blue-600 text-white px-4 py-2 rounded-md text-sm">
|
<Button
|
||||||
EDIT PROFILE
|
onClick={isEdit ? handleSave : () => setIsEdit(true)}
|
||||||
</button>
|
className="bg-blue-600 text-white px-4 py-2 rounded-md text-sm"
|
||||||
|
>
|
||||||
|
{isEdit ? "SAVE" : "EDIT"} PROFILE
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Profile Form */}
|
|
||||||
<section className="space-y-4">
|
<section className="space-y-4">
|
||||||
<h2 className="font-semibold text-gray-700">Profile</h2>
|
<h2 className="font-semibold text-gray-700">Profile</h2>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<Input
|
<InputClean
|
||||||
label="Nama Lengkap"
|
label="Nama Lengkap"
|
||||||
value="Prof. Dr. Albertus Wahyurudhanto, M.Si"
|
value={profile?.fullname ?? ""}
|
||||||
|
readOnly={!isEdit}
|
||||||
|
onChange={(v) => handleProfileChange("fullname", v)}
|
||||||
/>
|
/>
|
||||||
<Input label="Gelar" value="Profesor, Doctor, M.Si" />
|
|
||||||
<Input label="Pendidikan Terakhir" value="S3" />
|
<InputClean
|
||||||
<Input label="Pekerjaan Terakhir" value="Konsultan" />
|
label="Email"
|
||||||
<Input label="Email" value="albertus@example.com" />
|
value={profile?.email ?? ""}
|
||||||
<Input label="No Whatsapp" value="085112341234" />
|
readOnly={!isEdit}
|
||||||
<div className="col-span-2">
|
onChange={(v) => handleProfileChange("email", v)}
|
||||||
<label className="text-sm text-gray-600">Kata Sandi</label>
|
/>
|
||||||
<div className="relative">
|
|
||||||
<input
|
<InputClean
|
||||||
type={showPassword ? "text" : "password"}
|
label="No Whatsapp"
|
||||||
value="password123"
|
value={profile?.phoneNumber ?? ""}
|
||||||
disabled
|
readOnly={!isEdit}
|
||||||
className="w-full border rounded-md px-3 py-2 mt-1 text-sm bg-gray-50"
|
onChange={(v) => handleProfileChange("phoneNumber", v)}
|
||||||
/>
|
/>
|
||||||
<Eye
|
|
||||||
className="absolute right-3 top-3 h-4 w-4 cursor-pointer text-gray-500"
|
{isEdit && (
|
||||||
onClick={() => setShowPassword(!showPassword)}
|
<InputClean
|
||||||
/>
|
label="Password"
|
||||||
</div>
|
value={password}
|
||||||
</div>
|
readOnly={false}
|
||||||
|
onChange={setPassword}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
@ -77,34 +194,47 @@ export default function Profile() {
|
||||||
<section className="mt-8">
|
<section className="mt-8">
|
||||||
<h2 className="font-semibold text-gray-700 mb-4">Riwayat Pendidikan</h2>
|
<h2 className="font-semibold text-gray-700 mb-4">Riwayat Pendidikan</h2>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{[
|
{educationHistory.map((item, idx) => (
|
||||||
{
|
<div key={item.id} className="grid grid-cols-5 gap-4 items-end">
|
||||||
univ: "Universitas ABC",
|
<InputClean
|
||||||
jurusan: "Ilmu Komunikasi",
|
label="Universitas"
|
||||||
tingkat: "S1",
|
value={item.schoolName}
|
||||||
tahun: "2000",
|
readOnly={!isEdit}
|
||||||
},
|
onChange={(v) => handleEducationChange(idx, "schoolName", v)}
|
||||||
{
|
/>
|
||||||
univ: "Universitas ABC",
|
|
||||||
jurusan: "Ilmu Komunikasi",
|
<InputClean
|
||||||
tingkat: "S2",
|
label="Jurusan"
|
||||||
tahun: "2002",
|
value={item.major}
|
||||||
},
|
readOnly={!isEdit}
|
||||||
{
|
onChange={(v) => handleEducationChange(idx, "major", v)}
|
||||||
univ: "Universitas ABC",
|
/>
|
||||||
jurusan: "Ilmu Komunikasi",
|
|
||||||
tingkat: "S3",
|
<InputClean
|
||||||
tahun: "2004",
|
label="Tingkat Pendidikan"
|
||||||
},
|
value={item.educationLevel}
|
||||||
].map((item, i) => (
|
readOnly={!isEdit}
|
||||||
<div key={i} className="grid grid-cols-5 gap-4 items-end">
|
onChange={(v) =>
|
||||||
<Input label="Universitas" value={item.univ} />
|
handleEducationChange(idx, "educationLevel", v)
|
||||||
<Input label="Jurusan" value={item.jurusan} />
|
}
|
||||||
<Input label="Tingkat Pendidikan" value={item.tingkat} />
|
/>
|
||||||
<Input label="Tahun Lulus" value={item.tahun} />
|
|
||||||
<button className="bg-blue-600 text-white rounded-md px-3 py-2 text-sm flex items-center gap-2">
|
<InputClean
|
||||||
|
label="Tahun Lulus"
|
||||||
|
value={String(item.graduationYear)}
|
||||||
|
readOnly={!isEdit}
|
||||||
|
onChange={(v) =>
|
||||||
|
handleEducationChange(idx, "graduationYear", v)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href={item.certificateImage}
|
||||||
|
target="_blank"
|
||||||
|
className="bg-blue-600 text-white rounded-md px-3 py-2 text-sm flex items-center gap-2"
|
||||||
|
>
|
||||||
<Eye className="w-4 h-4" /> IJAZAH
|
<Eye className="w-4 h-4" /> IJAZAH
|
||||||
</button>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -113,63 +243,90 @@ export default function Profile() {
|
||||||
{/* Riwayat Pekerjaan */}
|
{/* Riwayat Pekerjaan */}
|
||||||
<section className="mt-8">
|
<section className="mt-8">
|
||||||
<h2 className="font-semibold text-gray-700 mb-4">Riwayat Pekerjaan</h2>
|
<h2 className="font-semibold text-gray-700 mb-4">Riwayat Pekerjaan</h2>
|
||||||
<div className="grid grid-cols-4 gap-4">
|
{workHistory.map((work, idx) => (
|
||||||
<Input
|
<div key={work.id} className="grid grid-cols-4 gap-4">
|
||||||
label="Title Pekerjaan"
|
<InputClean
|
||||||
value="Konsultan Komunikasi Pemerintahan"
|
label="Title Pekerjaan"
|
||||||
/>
|
value={work.jobTitle}
|
||||||
<Input label="Nama Perusahaan" value="Perusahaan ABC" />
|
readOnly={!isEdit}
|
||||||
<Input label="Tanggal Mulai" value="2000" />
|
onChange={(v) => handleWorkChange(idx, "jobTitle", v)}
|
||||||
<Input label="Tanggal Selesai" value="Sekarang" />
|
/>
|
||||||
</div>
|
|
||||||
|
<InputClean
|
||||||
|
label="Nama Perusahaan"
|
||||||
|
value={work.companyName}
|
||||||
|
readOnly={!isEdit}
|
||||||
|
onChange={(v) => handleWorkChange(idx, "companyName", v)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<InputClean
|
||||||
|
label="Tanggal Mulai"
|
||||||
|
value={convertDateFormatNoTimeV2(work.startDate)}
|
||||||
|
onChange={() => {}}
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
|
||||||
|
<InputClean
|
||||||
|
label="Tanggal Selesai"
|
||||||
|
value={convertDateFormatNoTimeV2(work.endDate)}
|
||||||
|
onChange={() => {}}
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Publikasi */}
|
{!isEdit && (
|
||||||
<section className="mt-8">
|
<section className="mt-8">
|
||||||
<h2 className="font-semibold text-gray-700 mb-4">Publikasi</h2>
|
<h2 className="font-semibold text-gray-700 mb-4">Publikasi</h2>
|
||||||
<table className="w-full border text-sm">
|
<table className="w-full border text-sm">
|
||||||
<thead className="bg-gray-50">
|
<thead className="bg-gray-50">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="border px-3 py-2 text-left">Judul</th>
|
<th className="border px-3 py-2 text-left">Judul</th>
|
||||||
<th className="border px-3 py-2 text-left">Publish</th>
|
<th className="border px-3 py-2 text-left">Publish</th>
|
||||||
<th className="border px-3 py-2 text-left">Tindakan</th>
|
<th className="border px-3 py-2 text-left">Tindakan</th>
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{[
|
|
||||||
{
|
|
||||||
judul: "Bunga rampai hukum ekonomi dan hukum internasional",
|
|
||||||
publish: "Sinta",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
judul: "Politik Hukum UU Bidang Ekonomi di Indonesia",
|
|
||||||
publish: "Sinta",
|
|
||||||
},
|
|
||||||
].map((item, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td className="border px-3 py-2">{item.judul}</td>
|
|
||||||
<td className="border px-3 py-2">{item.publish}</td>
|
|
||||||
<td className="border px-3 py-2 text-blue-600 cursor-pointer">
|
|
||||||
Lihat
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
</thead>
|
||||||
</tbody>
|
<tbody>
|
||||||
</table>
|
{researchJournals.map((item) => (
|
||||||
</section>
|
<tr key={item.id}>
|
||||||
|
<td className="border px-3 py-2">{item.journalTitle}</td>
|
||||||
|
<td className="border px-3 py-2">{item.publisher}</td>
|
||||||
|
<td className="border px-3 py-2 text-blue-600 cursor-pointer">
|
||||||
|
<Link target="_blank" href={item.journalUrl}>
|
||||||
|
Lihat
|
||||||
|
</Link>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reusable Input Component
|
// Reusable Input Component
|
||||||
function Input({ label, value }: { label: string; value: string }) {
|
function InputClean({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
readOnly = true,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
readOnly?: boolean;
|
||||||
|
onChange: (e: string) => void;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm text-gray-600">{label}</label>
|
<label className="text-sm text-gray-600">{label}</label>
|
||||||
<input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
value={value}
|
value={value}
|
||||||
disabled
|
readOnly={readOnly}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
className="w-full border rounded-md px-3 py-2 mt-1 text-sm bg-gray-50"
|
className="w-full border rounded-md px-3 py-2 mt-1 text-sm bg-gray-50"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -14,15 +14,26 @@ import {
|
||||||
} from "../ui/dialog";
|
} from "../ui/dialog";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import Navbar from "../navbar";
|
import Navbar from "../navbar";
|
||||||
|
import { getAllAgent } from "@/service/agent";
|
||||||
|
import { getUserExpert } from "@/service/user";
|
||||||
|
import { getCookiesDecrypt } from "@/utils/globals";
|
||||||
|
|
||||||
|
interface AgentData {
|
||||||
|
id: number;
|
||||||
|
agentId: string;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default function DashboardWelcome() {
|
export default function DashboardWelcome() {
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const [selectedExpert, setSelectedExpert] = useState("");
|
const [selectedExpert, setSelectedExpert] = useState("");
|
||||||
const [experts, setExperts] = useState<any[]>([]);
|
const [experts, setExperts] = useState<AgentData[]>([]);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const username = Cookies.get("username");
|
const username = Cookies.get("username");
|
||||||
|
const uie = getCookiesDecrypt("uie");
|
||||||
|
|
||||||
const questionTemplates = [
|
const questionTemplates = [
|
||||||
"Apa yang dimaksud dengan {topic}?",
|
"Apa yang dimaksud dengan {topic}?",
|
||||||
|
|
@ -56,34 +67,43 @@ export default function DashboardWelcome() {
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchExperts = async () => {
|
// const fetchExperts = async () => {
|
||||||
try {
|
// try {
|
||||||
const res = await fetch(
|
// // const res = await fetch(
|
||||||
"https://narasiahli.com/ai/api/v1/agents/?skip=0&limit=100&include_teams=true"
|
// // "https://narasiahli.com/ai/api/v1/agents/?skip=0&limit=100&include_teams=true",
|
||||||
);
|
// // );
|
||||||
const json = await res.json();
|
// // const json = await res.json();
|
||||||
// console.log("DATA EXPERTS:", json);
|
// const res = await getAllAgent();
|
||||||
const filtered = json.filter(
|
// const json = res?.data?.data;
|
||||||
(a: any) =>
|
// // console.log("DATA EXPERTS:", json);
|
||||||
a.name.includes("Prof. ") ||
|
// const filtered = json.filter(
|
||||||
a.name.includes("Dr. ") ||
|
// (a: any) =>
|
||||||
a.name.includes("Ir.") ||
|
// a.name.includes("Prof. ") ||
|
||||||
a.name.includes("M.Pd") ||
|
// a.name.includes("Dr. ") ||
|
||||||
a.name.includes("Kementerian") ||
|
// a.name.includes("Ir.") ||
|
||||||
a.name.includes("Polri") ||
|
// a.name.includes("M.Pd") ||
|
||||||
a.name.includes("MediaHub") ||
|
// a.name.includes("Kementerian") ||
|
||||||
a.name.includes("Korlantas") ||
|
// a.name.includes("Polri") ||
|
||||||
a.name.includes("KUHP")
|
// a.name.includes("MediaHub") ||
|
||||||
);
|
// a.name.includes("Korlantas") ||
|
||||||
setExperts(filtered || []);
|
// a.name.includes("KUHP"),
|
||||||
} catch (err) {
|
// );
|
||||||
console.error("Gagal mengambil data tenaga ahli:", err);
|
// setExperts(filtered || []);
|
||||||
}
|
// } catch (err) {
|
||||||
};
|
// console.error("Gagal mengambil data tenaga ahli:", err);
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
fetchExperts();
|
// fetchExperts();
|
||||||
|
getUserExpertData();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const getUserExpertData = async () => {
|
||||||
|
const res = await getUserExpert(Number(uie));
|
||||||
|
const data: AgentData[] = res?.data?.data.agentId ?? [];
|
||||||
|
setExperts(data);
|
||||||
|
};
|
||||||
|
|
||||||
const handleContinue = async () => {
|
const handleContinue = async () => {
|
||||||
if (!selectedExpert) return;
|
if (!selectedExpert) return;
|
||||||
const randomQuery = getRandomQuestion();
|
const randomQuery = getRandomQuestion();
|
||||||
|
|
@ -105,7 +125,7 @@ export default function DashboardWelcome() {
|
||||||
|
|
||||||
const filteredExperts = useMemo(() => {
|
const filteredExperts = useMemo(() => {
|
||||||
const filteredExperts = experts.filter((expert) =>
|
const filteredExperts = experts.filter((expert) =>
|
||||||
expert.name?.toLowerCase().includes(search.toLowerCase())
|
expert.name?.toLowerCase().includes(search.toLowerCase()),
|
||||||
);
|
);
|
||||||
return filteredExperts ? filteredExperts : [];
|
return filteredExperts ? filteredExperts : [];
|
||||||
}, [search, experts]);
|
}, [search, experts]);
|
||||||
|
|
@ -163,11 +183,11 @@ export default function DashboardWelcome() {
|
||||||
<div className="space-y-2 max-h-64 overflow-y-auto pr-1">
|
<div className="space-y-2 max-h-64 overflow-y-auto pr-1">
|
||||||
{filteredExperts.map((expert: any) => (
|
{filteredExperts.map((expert: any) => (
|
||||||
<div
|
<div
|
||||||
key={expert.id}
|
key={expert.agentId}
|
||||||
className={`flex items-center gap-3 p-2 border rounded-md cursor-pointer hover:bg-gray-50 transition-colors ${
|
className={`flex items-center gap-3 p-2 border rounded-md cursor-pointer hover:bg-gray-50 transition-colors ${
|
||||||
selectedExpert === expert.id ? "border-blue-500" : ""
|
selectedExpert === expert.agentId ? "border-blue-500" : ""
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setSelectedExpert(expert.id)}
|
onClick={() => setSelectedExpert(expert.agentId)}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={expert.image_url || "/profile.png"}
|
src={expert.image_url || "/profile.png"}
|
||||||
|
|
@ -176,14 +196,20 @@ export default function DashboardWelcome() {
|
||||||
height={48}
|
height={48}
|
||||||
className="w-12 h-12 rounded-full object-cover"
|
className="w-12 h-12 rounded-full object-cover"
|
||||||
/>
|
/>
|
||||||
<span className="text-sm line-clamp-2">{expert.name}</span>
|
<div className="flex flex-col gap-1">
|
||||||
|
{" "}
|
||||||
|
<span className="text-sm line-clamp-2 font-semibold">
|
||||||
|
{expert.name}
|
||||||
|
</span>
|
||||||
|
<p className="text-xs uppercase">{expert.type}</p>
|
||||||
|
</div>
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
name="expert"
|
name="expert"
|
||||||
value={expert.id}
|
value={expert.agentId}
|
||||||
className="ml-auto accent-blue-600"
|
className="ml-auto accent-blue-600"
|
||||||
checked={selectedExpert === expert.id}
|
checked={selectedExpert === expert.agentId}
|
||||||
onChange={() => setSelectedExpert(expert.id)}
|
onChange={() => setSelectedExpert(expert.agentId)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,14 @@ import { useRouter } from "next/navigation";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { getCookiesDecrypt } from "@/utils/globals";
|
import { getCookiesDecrypt } from "@/utils/globals";
|
||||||
import { uploadKnowledgeBase } from "@/service/data-knowledge";
|
import { uploadKnowledgeBase } from "@/service/data-knowledge";
|
||||||
|
import { getUserExpert } from "@/service/user";
|
||||||
|
|
||||||
|
interface AgentData {
|
||||||
|
id: number;
|
||||||
|
agentId: string;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default function CreateKnowledgeBase() {
|
export default function CreateKnowledgeBase() {
|
||||||
const MySwal = withReactContent(Swal);
|
const MySwal = withReactContent(Swal);
|
||||||
|
|
@ -46,14 +54,15 @@ export default function CreateKnowledgeBase() {
|
||||||
const [videoFile, setVideoFile] = useState<File | null>(null);
|
const [videoFile, setVideoFile] = useState<File | null>(null);
|
||||||
const [audioFile, setAudioFile] = useState<File | null>(null);
|
const [audioFile, setAudioFile] = useState<File | null>(null);
|
||||||
const uie = getCookiesDecrypt("uie");
|
const uie = getCookiesDecrypt("uie");
|
||||||
|
const urie = getCookiesDecrypt("urie");
|
||||||
|
|
||||||
const [dragging, setDragging] = useState<"doc" | "video" | "audio" | null>(
|
const [dragging, setDragging] = useState<"doc" | "video" | "audio" | null>(
|
||||||
null
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDropFile = (
|
const handleDropFile = (
|
||||||
e: React.DragEvent<HTMLDivElement>,
|
e: React.DragEvent<HTMLDivElement>,
|
||||||
type: "doc" | "video" | "audio"
|
type: "doc" | "video" | "audio",
|
||||||
) => {
|
) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
@ -101,25 +110,35 @@ export default function CreateKnowledgeBase() {
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchExperts = async () => {
|
if (urie == "1") {
|
||||||
try {
|
fetchExperts();
|
||||||
const res = await fetch(
|
} else {
|
||||||
"https://narasiahli.com/ai/api/v1/agents/?skip=0&limit=100&include_teams=true"
|
getUserExpertData();
|
||||||
);
|
}
|
||||||
const json = await res.json();
|
|
||||||
|
|
||||||
setExperts(json || []);
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Gagal mengambil data tenaga ahli:", err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchExperts();
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const fetchExperts = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
"https://narasiahli.com/ai/api/v1/agents/?skip=0&limit=100&include_teams=true",
|
||||||
|
);
|
||||||
|
const json = await res.json();
|
||||||
|
|
||||||
|
setExperts(json || []);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal mengambil data tenaga ahli:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUserExpertData = async () => {
|
||||||
|
const res = await getUserExpert(Number(uie));
|
||||||
|
const data: AgentData[] = res?.data?.data.agentId ?? [];
|
||||||
|
setExperts(data);
|
||||||
|
};
|
||||||
|
|
||||||
const filteredExperts = useMemo(() => {
|
const filteredExperts = useMemo(() => {
|
||||||
const filteredExperts = experts.filter((expert) =>
|
const filteredExperts = experts.filter((expert) =>
|
||||||
expert.name?.toLowerCase().includes(search.toLowerCase())
|
expert.name?.toLowerCase().includes(search.toLowerCase()),
|
||||||
);
|
);
|
||||||
return filteredExperts ? filteredExperts : [];
|
return filteredExperts ? filteredExperts : [];
|
||||||
}, [search, experts]);
|
}, [search, experts]);
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,16 @@ export default function Login() {
|
||||||
Cookies.set("ufne", profile?.data?.data?.fullname, { expires: 1 });
|
Cookies.set("ufne", profile?.data?.data?.fullname, { expires: 1 });
|
||||||
Cookies.set("username", profile?.data?.data?.username, { expires: 1 });
|
Cookies.set("username", profile?.data?.data?.username, { expires: 1 });
|
||||||
Cookies.set("status", "login", { expires: 1 });
|
Cookies.set("status", "login", { expires: 1 });
|
||||||
router.push("/admin/dashboard");
|
if (profile?.data?.data?.userRoleId == 1) {
|
||||||
|
router.push("/admin/management-user");
|
||||||
|
} else if (
|
||||||
|
profile?.data?.data?.userRoleId == 3 &&
|
||||||
|
profile?.data?.data?.userLevelId === 2
|
||||||
|
) {
|
||||||
|
router.push("/admin/data-knowledge");
|
||||||
|
} else {
|
||||||
|
router.push("/admin/dashboard");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ export default function ScheduleForm() {
|
||||||
|
|
||||||
const loadUsers = async () => {
|
const loadUsers = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await getAllUsers();
|
const response = await getAllUsers({});
|
||||||
if (!response.error && response.data?.data) {
|
if (!response.error && response.data?.data) {
|
||||||
setUsers(response.data.data);
|
setUsers(response.data.data);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -94,7 +94,7 @@ export default function ScheduleForm() {
|
||||||
|
|
||||||
const handleFileChange = (
|
const handleFileChange = (
|
||||||
fileType: "journal" | "video" | "audio",
|
fileType: "journal" | "video" | "audio",
|
||||||
file: File | null
|
file: File | null,
|
||||||
) => {
|
) => {
|
||||||
setFormData((prev) => ({
|
setFormData((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,25 @@ import { useRouter } from "next/navigation";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { getUserDetail, getUserWorkHistory, getUserEducationHistory } from "@/service/user";
|
import {
|
||||||
|
getUserDetail,
|
||||||
|
getUserWorkHistory,
|
||||||
|
getUserEducationHistory,
|
||||||
|
} from "@/service/user";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Edit, User, Mail, Phone, MapPin, Calendar, GraduationCap, Briefcase, Clock } from "lucide-react";
|
import {
|
||||||
|
Edit,
|
||||||
|
User,
|
||||||
|
Mail,
|
||||||
|
Phone,
|
||||||
|
MapPin,
|
||||||
|
Calendar,
|
||||||
|
GraduationCap,
|
||||||
|
Briefcase,
|
||||||
|
Clock,
|
||||||
|
} from "lucide-react";
|
||||||
|
import Navbar from "../navbar";
|
||||||
|
|
||||||
interface UserDetailProps {
|
interface UserDetailProps {
|
||||||
userId: number;
|
userId: number;
|
||||||
|
|
@ -36,7 +51,9 @@ export default function UserDetail({ userId }: UserDetailProps) {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [userData, setUserData] = useState<any>(null);
|
const [userData, setUserData] = useState<any>(null);
|
||||||
const [workHistory, setWorkHistory] = useState<WorkHistory[]>([]);
|
const [workHistory, setWorkHistory] = useState<WorkHistory[]>([]);
|
||||||
const [educationHistory, setEducationHistory] = useState<EducationHistory[]>([]);
|
const [educationHistory, setEducationHistory] = useState<EducationHistory[]>(
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
|
|
@ -49,16 +66,16 @@ export default function UserDetail({ userId }: UserDetailProps) {
|
||||||
const response = await getUserDetail(userId);
|
const response = await getUserDetail(userId);
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
setUserData(response.data);
|
setUserData(response.data);
|
||||||
|
|
||||||
// Load work and education history if user is tenaga ahli
|
// Load work and education history if user is tenaga ahli
|
||||||
if (response.data.user_role_id === 2) {
|
if (response.data?.userRoleId === 3) {
|
||||||
const workResponse = await getUserWorkHistory(userId);
|
const workResponse = await getUserWorkHistory(userId);
|
||||||
const educationResponse = await getUserEducationHistory(userId);
|
const educationResponse = await getUserEducationHistory(userId);
|
||||||
|
|
||||||
if (!workResponse.error && workResponse.data?.data) {
|
if (!workResponse.error && workResponse.data?.data) {
|
||||||
setWorkHistory(workResponse.data.data);
|
setWorkHistory(workResponse.data.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!educationResponse.error && educationResponse.data?.data) {
|
if (!educationResponse.error && educationResponse.data?.data) {
|
||||||
setEducationHistory(educationResponse.data.data);
|
setEducationHistory(educationResponse.data.data);
|
||||||
}
|
}
|
||||||
|
|
@ -76,20 +93,20 @@ export default function UserDetail({ userId }: UserDetailProps) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatDate = (dateString: string) => {
|
const formatDate = (dateString: string) => {
|
||||||
return new Date(dateString).toLocaleDateString('id-ID', {
|
return new Date(dateString).toLocaleDateString("id-ID", {
|
||||||
year: 'numeric',
|
year: "numeric",
|
||||||
month: 'long',
|
month: "long",
|
||||||
day: 'numeric',
|
day: "numeric",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatDateTime = (dateString: string) => {
|
const formatDateTime = (dateString: string) => {
|
||||||
return new Date(dateString).toLocaleDateString('id-ID', {
|
return new Date(dateString).toLocaleDateString("id-ID", {
|
||||||
year: 'numeric',
|
year: "numeric",
|
||||||
month: 'long',
|
month: "long",
|
||||||
day: 'numeric',
|
day: "numeric",
|
||||||
hour: '2-digit',
|
hour: "2-digit",
|
||||||
minute: '2-digit',
|
minute: "2-digit",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -140,9 +157,12 @@ export default function UserDetail({ userId }: UserDetailProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-6xl mx-auto space-y-6">
|
<div className="flex-1 overflow-hidden flex flex-col h-[90vh]">
|
||||||
{/* Header */}
|
<Navbar
|
||||||
<div className="flex items-center justify-between">
|
title="Management User,/admin/management-user"
|
||||||
|
subTitle="Detail User"
|
||||||
|
/>
|
||||||
|
<div className="flex items-center justify-between mt-10">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-semibold">{userData.fullname}</h1>
|
<h1 className="text-2xl font-semibold">{userData.fullname}</h1>
|
||||||
<p className="text-gray-600">Detail informasi user</p>
|
<p className="text-gray-600">Detail informasi user</p>
|
||||||
|
|
@ -174,47 +194,65 @@ export default function UserDetail({ userId }: UserDetailProps) {
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium text-gray-500">Nama Lengkap</label>
|
<label className="text-sm font-medium text-gray-500">
|
||||||
|
Nama Lengkap
|
||||||
|
</label>
|
||||||
<p className="text-lg font-semibold">{userData.fullname}</p>
|
<p className="text-lg font-semibold">{userData.fullname}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium text-gray-500">Username</label>
|
<label className="text-sm font-medium text-gray-500">
|
||||||
<p className="text-sm font-mono bg-gray-100 p-2 rounded">{userData.username}</p>
|
Username
|
||||||
|
</label>
|
||||||
|
<p className="text-sm font-mono bg-gray-100 p-2 rounded">
|
||||||
|
{userData.username}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium text-gray-500">Email</label>
|
<label className="text-sm font-medium text-gray-500">
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
<p className="text-sm flex items-center space-x-2">
|
<p className="text-sm flex items-center space-x-2">
|
||||||
<Mail className="w-4 h-4" />
|
<Mail className="w-4 h-4" />
|
||||||
<span>{userData.email}</span>
|
<span>{userData.email}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium text-gray-500">No Telepon</label>
|
<label className="text-sm font-medium text-gray-500">
|
||||||
|
No Telepon
|
||||||
|
</label>
|
||||||
<p className="text-sm flex items-center space-x-2">
|
<p className="text-sm flex items-center space-x-2">
|
||||||
<Phone className="w-4 h-4" />
|
<Phone className="w-4 h-4" />
|
||||||
<span>{userData.phone_number}</span>
|
<span>{userData.phone_number}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium text-gray-500">No Whatsapp</label>
|
<label className="text-sm font-medium text-gray-500">
|
||||||
|
No Whatsapp
|
||||||
|
</label>
|
||||||
<p className="text-sm flex items-center space-x-2">
|
<p className="text-sm flex items-center space-x-2">
|
||||||
<Phone className="w-4 h-4" />
|
<Phone className="w-4 h-4" />
|
||||||
<span>{userData.whatsapp_number}</span>
|
<span>{userData.whatsapp_number}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium text-gray-500">Tanggal Lahir</label>
|
<label className="text-sm font-medium text-gray-500">
|
||||||
|
Tanggal Lahir
|
||||||
|
</label>
|
||||||
<p className="text-sm flex items-center space-x-2">
|
<p className="text-sm flex items-center space-x-2">
|
||||||
<Calendar className="w-4 h-4" />
|
<Calendar className="w-4 h-4" />
|
||||||
<span>{formatDate(userData.date_of_birth)}</span>
|
<span>{formatDate(userData.date_of_birth)}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium text-gray-500">Jenis Kelamin</label>
|
<label className="text-sm font-medium text-gray-500">
|
||||||
|
Jenis Kelamin
|
||||||
|
</label>
|
||||||
<p className="text-sm capitalize">{userData.gender_type}</p>
|
<p className="text-sm capitalize">{userData.gender_type}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium text-gray-500">Alamat</label>
|
<label className="text-sm font-medium text-gray-500">
|
||||||
|
Alamat
|
||||||
|
</label>
|
||||||
<p className="text-sm flex items-center space-x-2">
|
<p className="text-sm flex items-center space-x-2">
|
||||||
<MapPin className="w-4 h-4" />
|
<MapPin className="w-4 h-4" />
|
||||||
<span>{userData.address}</span>
|
<span>{userData.address}</span>
|
||||||
|
|
@ -222,7 +260,9 @@ export default function UserDetail({ userId }: UserDetailProps) {
|
||||||
</div>
|
</div>
|
||||||
{userData.degree && (
|
{userData.degree && (
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium text-gray-500">Gelar</label>
|
<label className="text-sm font-medium text-gray-500">
|
||||||
|
Gelar
|
||||||
|
</label>
|
||||||
<p className="text-sm">{userData.degree}</p>
|
<p className="text-sm">{userData.degree}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -245,19 +285,29 @@ export default function UserDetail({ userId }: UserDetailProps) {
|
||||||
<div key={education.id} className="border rounded-lg p-4">
|
<div key={education.id} className="border rounded-lg p-4">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium text-gray-500">Nama Sekolah/Universitas</label>
|
<label className="text-sm font-medium text-gray-500">
|
||||||
<p className="text-sm font-semibold">{education.school_name}</p>
|
Nama Sekolah/Universitas
|
||||||
|
</label>
|
||||||
|
<p className="text-sm font-semibold">
|
||||||
|
{education.school_name}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium text-gray-500">Jurusan</label>
|
<label className="text-sm font-medium text-gray-500">
|
||||||
|
Jurusan
|
||||||
|
</label>
|
||||||
<p className="text-sm">{education.major}</p>
|
<p className="text-sm">{education.major}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium text-gray-500">Tingkat Pendidikan</label>
|
<label className="text-sm font-medium text-gray-500">
|
||||||
|
Tingkat Pendidikan
|
||||||
|
</label>
|
||||||
<p className="text-sm">{education.education_level}</p>
|
<p className="text-sm">{education.education_level}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium text-gray-500">Tahun Lulus</label>
|
<label className="text-sm font-medium text-gray-500">
|
||||||
|
Tahun Lulus
|
||||||
|
</label>
|
||||||
<p className="text-sm">{education.graduation_year}</p>
|
<p className="text-sm">{education.graduation_year}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -283,22 +333,32 @@ export default function UserDetail({ userId }: UserDetailProps) {
|
||||||
<div key={work.id} className="border rounded-lg p-4">
|
<div key={work.id} className="border rounded-lg p-4">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium text-gray-500">Jabatan</label>
|
<label className="text-sm font-medium text-gray-500">
|
||||||
<p className="text-sm font-semibold">{work.job_title}</p>
|
Jabatan
|
||||||
|
</label>
|
||||||
|
<p className="text-sm font-semibold">
|
||||||
|
{work.job_title}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium text-gray-500">Perusahaan</label>
|
<label className="text-sm font-medium text-gray-500">
|
||||||
|
Perusahaan
|
||||||
|
</label>
|
||||||
<p className="text-sm">{work.company_name}</p>
|
<p className="text-sm">{work.company_name}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium text-gray-500">Tanggal Mulai</label>
|
<label className="text-sm font-medium text-gray-500">
|
||||||
|
Tanggal Mulai
|
||||||
|
</label>
|
||||||
<p className="text-sm flex items-center space-x-2">
|
<p className="text-sm flex items-center space-x-2">
|
||||||
<Calendar className="w-4 h-4" />
|
<Calendar className="w-4 h-4" />
|
||||||
<span>{formatDate(work.start_date)}</span>
|
<span>{formatDate(work.start_date)}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium text-gray-500">Tanggal Selesai</label>
|
<label className="text-sm font-medium text-gray-500">
|
||||||
|
Tanggal Selesai
|
||||||
|
</label>
|
||||||
<p className="text-sm flex items-center space-x-2">
|
<p className="text-sm flex items-center space-x-2">
|
||||||
<Calendar className="w-4 h-4" />
|
<Calendar className="w-4 h-4" />
|
||||||
<span>{formatDate(work.end_date)}</span>
|
<span>{formatDate(work.end_date)}</span>
|
||||||
|
|
@ -329,11 +389,15 @@ export default function UserDetail({ userId }: UserDetailProps) {
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-sm text-gray-500">Jenis Akun</span>
|
<span className="text-sm text-gray-500">Jenis Akun</span>
|
||||||
<span className="font-semibold">{getUserRoleName(userData.user_role_id)}</span>
|
<span className="font-semibold">
|
||||||
|
{getUserRoleName(userData.user_role_id)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-sm text-gray-500">Level</span>
|
<span className="text-sm text-gray-500">Level</span>
|
||||||
<span className="font-semibold">{getUserLevelName(userData.user_level_id)}</span>
|
<span className="font-semibold">
|
||||||
|
{getUserLevelName(userData.user_level_id)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
@ -345,7 +409,9 @@ export default function UserDetail({ userId }: UserDetailProps) {
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium text-gray-500">Keycloak ID</label>
|
<label className="text-sm font-medium text-gray-500">
|
||||||
|
Keycloak ID
|
||||||
|
</label>
|
||||||
<p className="text-xs font-mono bg-gray-100 p-2 rounded mt-1 break-all">
|
<p className="text-xs font-mono bg-gray-100 p-2 rounded mt-1 break-all">
|
||||||
{userData.keycloak_id}
|
{userData.keycloak_id}
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -369,12 +435,20 @@ export default function UserDetail({ userId }: UserDetailProps) {
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium text-gray-500">Dibuat</label>
|
<label className="text-sm font-medium text-gray-500">
|
||||||
<p className="text-sm mt-1">{formatDateTime(userData.created_at)}</p>
|
Dibuat
|
||||||
|
</label>
|
||||||
|
<p className="text-sm mt-1">
|
||||||
|
{formatDateTime(userData.created_at)}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium text-gray-500">Diperbarui</label>
|
<label className="text-sm font-medium text-gray-500">
|
||||||
<p className="text-sm mt-1">{formatDateTime(userData.updated_at)}</p>
|
Diperbarui
|
||||||
|
</label>
|
||||||
|
<p className="text-sm mt-1">
|
||||||
|
{formatDateTime(userData.updated_at)}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,25 @@ import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import {
|
||||||
import { createUser, updateUser, getUserDetail, createWorkHistory, createEducationHistory, UserData, WorkHistoryData, EducationHistoryData } from "@/service/user";
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import {
|
||||||
|
createUser,
|
||||||
|
updateUser,
|
||||||
|
getUserDetail,
|
||||||
|
createWorkHistory,
|
||||||
|
createEducationHistory,
|
||||||
|
UserData,
|
||||||
|
WorkHistoryData,
|
||||||
|
EducationHistoryData,
|
||||||
|
} from "@/service/user";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
|
import Navbar from "../navbar";
|
||||||
|
|
||||||
interface UserFormProps {
|
interface UserFormProps {
|
||||||
userId?: number;
|
userId?: number;
|
||||||
|
|
@ -33,7 +49,9 @@ export default function UserForm({ userId, mode }: UserFormProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [userType, setUserType] = useState<"tenaga_ahli" | "pengguna_umum">("tenaga_ahli");
|
const [userType, setUserType] = useState<"tenaga_ahli" | "pengguna_umum">(
|
||||||
|
"tenaga_ahli",
|
||||||
|
);
|
||||||
const [educationList, setEducationList] = useState<EducationItem[]>([
|
const [educationList, setEducationList] = useState<EducationItem[]>([
|
||||||
{ schoolName: "", major: "", educationLevel: "", graduationYear: "" },
|
{ schoolName: "", major: "", educationLevel: "", graduationYear: "" },
|
||||||
]);
|
]);
|
||||||
|
|
@ -51,7 +69,7 @@ export default function UserForm({ userId, mode }: UserFormProps) {
|
||||||
dateOfBirth: "",
|
dateOfBirth: "",
|
||||||
genderType: "male",
|
genderType: "male",
|
||||||
degree: "",
|
degree: "",
|
||||||
userLevelId: 1,
|
userLevelId: 3,
|
||||||
userRoleId: 3,
|
userRoleId: 3,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -69,23 +87,24 @@ export default function UserForm({ userId, mode }: UserFormProps) {
|
||||||
const response = await getUserDetail(userId!);
|
const response = await getUserDetail(userId!);
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
const data = response.data;
|
const data = response.data;
|
||||||
|
|
||||||
setFormData({
|
setFormData({
|
||||||
username: data.username,
|
username: data.username,
|
||||||
email: data.email,
|
email: data.email,
|
||||||
fullname: data.fullname,
|
fullname: data.fullname,
|
||||||
address: data.address,
|
address: data.address,
|
||||||
phoneNumber: data.phone_number,
|
phoneNumber: data.phoneNumber,
|
||||||
whatsappNumber: data.whatsapp_number,
|
whatsappNumber: data.whatsappNumber,
|
||||||
password: "", // Don't load password for security
|
password: "", // Don't load password for security
|
||||||
dateOfBirth: data.date_of_birth,
|
dateOfBirth: data.dateOfBirth,
|
||||||
genderType: data.gender_type as "male" | "female",
|
genderType: data.genderType as "male" | "female",
|
||||||
degree: data.degree || "",
|
degree: data.degree || "",
|
||||||
userLevelId: data.user_level_id,
|
userLevelId: data.userLevelId,
|
||||||
userRoleId: data.user_role_id,
|
userRoleId: data.userRoleId,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set user type based on role
|
// Set user type based on role
|
||||||
setUserType(data.user_role_id === 2 ? "tenaga_ahli" : "pengguna_umum");
|
setUserType(data.userRoleId === 2 ? "tenaga_ahli" : "pengguna_umum");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error("Gagal memuat data user");
|
toast.error("Gagal memuat data user");
|
||||||
|
|
@ -95,9 +114,9 @@ export default function UserForm({ userId, mode }: UserFormProps) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleInputChange = (field: keyof UserData, value: any) => {
|
const handleInputChange = (field: keyof UserData, value: any) => {
|
||||||
setFormData(prev => ({
|
setFormData((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[field]: value
|
[field]: value,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -115,13 +134,21 @@ export default function UserForm({ userId, mode }: UserFormProps) {
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEducationChange = (index: number, field: keyof EducationItem, value: string) => {
|
const handleEducationChange = (
|
||||||
|
index: number,
|
||||||
|
field: keyof EducationItem,
|
||||||
|
value: string,
|
||||||
|
) => {
|
||||||
const updated = [...educationList];
|
const updated = [...educationList];
|
||||||
updated[index][field] = value;
|
updated[index][field] = value;
|
||||||
setEducationList(updated);
|
setEducationList(updated);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleWorkChange = (index: number, field: keyof WorkItem, value: string) => {
|
const handleWorkChange = (
|
||||||
|
index: number,
|
||||||
|
field: keyof WorkItem,
|
||||||
|
value: string,
|
||||||
|
) => {
|
||||||
const updated = [...workList];
|
const updated = [...workList];
|
||||||
updated[index][field] = value;
|
updated[index][field] = value;
|
||||||
setWorkList(updated);
|
setWorkList(updated);
|
||||||
|
|
@ -129,15 +156,15 @@ export default function UserForm({ userId, mode }: UserFormProps) {
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
// Set user role and level based on user type
|
// Set user role and level based on user type
|
||||||
const userData = {
|
const userData = {
|
||||||
...formData,
|
...formData,
|
||||||
userRoleId: userType === "tenaga_ahli" ? 2 : 3,
|
userRoleId: userType === "tenaga_ahli" ? 3 : 4,
|
||||||
userLevelId: userType === "tenaga_ahli" ? 2 : 1,
|
userLevelId: userType === "tenaga_ahli" ? 3 : 4,
|
||||||
};
|
};
|
||||||
|
|
||||||
let response;
|
let response;
|
||||||
|
|
@ -156,11 +183,16 @@ export default function UserForm({ userId, mode }: UserFormProps) {
|
||||||
|
|
||||||
if (!response?.error) {
|
if (!response?.error) {
|
||||||
const userId = response?.data?.data?.id;
|
const userId = response?.data?.data?.id;
|
||||||
|
|
||||||
if (userId && userType === "tenaga_ahli") {
|
if (userId && userType === "tenaga_ahli") {
|
||||||
// Create education history
|
// Create education history
|
||||||
for (const education of educationList) {
|
for (const education of educationList) {
|
||||||
if (education.schoolName && education.major && education.educationLevel && education.graduationYear) {
|
if (
|
||||||
|
education.schoolName &&
|
||||||
|
education.major &&
|
||||||
|
education.educationLevel &&
|
||||||
|
education.graduationYear
|
||||||
|
) {
|
||||||
const educationData: EducationHistoryData = {
|
const educationData: EducationHistoryData = {
|
||||||
userId: userId,
|
userId: userId,
|
||||||
schoolName: education.schoolName,
|
schoolName: education.schoolName,
|
||||||
|
|
@ -174,7 +206,12 @@ export default function UserForm({ userId, mode }: UserFormProps) {
|
||||||
|
|
||||||
// Create work history
|
// Create work history
|
||||||
for (const work of workList) {
|
for (const work of workList) {
|
||||||
if (work.jobTitle && work.companyName && work.startDate && work.endDate) {
|
if (
|
||||||
|
work.jobTitle &&
|
||||||
|
work.companyName &&
|
||||||
|
work.startDate &&
|
||||||
|
work.endDate
|
||||||
|
) {
|
||||||
const workData: WorkHistoryData = {
|
const workData: WorkHistoryData = {
|
||||||
userId: userId,
|
userId: userId,
|
||||||
jobTitle: work.jobTitle,
|
jobTitle: work.jobTitle,
|
||||||
|
|
@ -187,7 +224,9 @@ export default function UserForm({ userId, mode }: UserFormProps) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.success(`User berhasil ${mode === "create" ? "dibuat" : "diperbarui"}`);
|
toast.success(
|
||||||
|
`User berhasil ${mode === "create" ? "dibuat" : "diperbarui"}`,
|
||||||
|
);
|
||||||
router.push("/admin/management-user");
|
router.push("/admin/management-user");
|
||||||
} else {
|
} else {
|
||||||
toast.error(response.message || "Terjadi kesalahan");
|
toast.error(response.message || "Terjadi kesalahan");
|
||||||
|
|
@ -216,15 +255,17 @@ export default function UserForm({ userId, mode }: UserFormProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-4xl mx-auto space-y-6">
|
<div className="flex-1 overflow-hidden flex flex-col h-[90vh]">
|
||||||
<div className="flex items-center justify-between">
|
<Navbar
|
||||||
|
title="Management User,/admin/management-user"
|
||||||
|
subTitle="Edit User"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between mt-10">
|
||||||
<h1 className="text-2xl font-semibold">
|
<h1 className="text-2xl font-semibold">
|
||||||
{mode === "create" ? "Tambah User Baru" : "Edit User"}
|
{mode === "create" ? "Tambah User Baru" : "Edit User"}
|
||||||
</h1>
|
</h1>
|
||||||
<Button
|
<Button variant="outline" onClick={() => router.back()}>
|
||||||
variant="outline"
|
|
||||||
onClick={() => router.back()}
|
|
||||||
>
|
|
||||||
Kembali
|
Kembali
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -235,7 +276,9 @@ export default function UserForm({ userId, mode }: UserFormProps) {
|
||||||
<h2 className="text-lg font-semibold mb-4">Jenis Pengguna</h2>
|
<h2 className="text-lg font-semibold mb-4">Jenis Pengguna</h2>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
value={userType}
|
value={userType}
|
||||||
onValueChange={(value: "tenaga_ahli" | "pengguna_umum") => setUserType(value)}
|
onValueChange={(value: "tenaga_ahli" | "pengguna_umum") =>
|
||||||
|
setUserType(value)
|
||||||
|
}
|
||||||
className="flex space-x-6"
|
className="flex space-x-6"
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
|
|
@ -300,7 +343,9 @@ export default function UserForm({ userId, mode }: UserFormProps) {
|
||||||
<Input
|
<Input
|
||||||
id="phoneNumber"
|
id="phoneNumber"
|
||||||
value={formData.phoneNumber}
|
value={formData.phoneNumber}
|
||||||
onChange={(e) => handleInputChange("phoneNumber", e.target.value)}
|
onChange={(e) =>
|
||||||
|
handleInputChange("phoneNumber", e.target.value)
|
||||||
|
}
|
||||||
placeholder="Masukkan No Telepon"
|
placeholder="Masukkan No Telepon"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
@ -310,7 +355,9 @@ export default function UserForm({ userId, mode }: UserFormProps) {
|
||||||
<Input
|
<Input
|
||||||
id="whatsappNumber"
|
id="whatsappNumber"
|
||||||
value={formData.whatsappNumber}
|
value={formData.whatsappNumber}
|
||||||
onChange={(e) => handleInputChange("whatsappNumber", e.target.value)}
|
onChange={(e) =>
|
||||||
|
handleInputChange("whatsappNumber", e.target.value)
|
||||||
|
}
|
||||||
placeholder="Masukkan No Whatsapp"
|
placeholder="Masukkan No Whatsapp"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
@ -321,7 +368,9 @@ export default function UserForm({ userId, mode }: UserFormProps) {
|
||||||
id="dateOfBirth"
|
id="dateOfBirth"
|
||||||
type="date"
|
type="date"
|
||||||
value={formData.dateOfBirth}
|
value={formData.dateOfBirth}
|
||||||
onChange={(e) => handleInputChange("dateOfBirth", e.target.value)}
|
onChange={(e) =>
|
||||||
|
handleInputChange("dateOfBirth", e.target.value)
|
||||||
|
}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -329,7 +378,9 @@ export default function UserForm({ userId, mode }: UserFormProps) {
|
||||||
<Label htmlFor="genderType">Jenis Kelamin *</Label>
|
<Label htmlFor="genderType">Jenis Kelamin *</Label>
|
||||||
<Select
|
<Select
|
||||||
value={formData.genderType}
|
value={formData.genderType}
|
||||||
onValueChange={(value: "male" | "female") => handleInputChange("genderType", value)}
|
onValueChange={(value: "male" | "female") =>
|
||||||
|
handleInputChange("genderType", value)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Pilih Jenis Kelamin" />
|
<SelectValue placeholder="Pilih Jenis Kelamin" />
|
||||||
|
|
@ -357,7 +408,9 @@ export default function UserForm({ userId, mode }: UserFormProps) {
|
||||||
id="password"
|
id="password"
|
||||||
type="password"
|
type="password"
|
||||||
value={formData.password}
|
value={formData.password}
|
||||||
onChange={(e) => handleInputChange("password", e.target.value)}
|
onChange={(e) =>
|
||||||
|
handleInputChange("password", e.target.value)
|
||||||
|
}
|
||||||
placeholder="Masukkan Kata Sandi"
|
placeholder="Masukkan Kata Sandi"
|
||||||
required={mode === "create"}
|
required={mode === "create"}
|
||||||
/>
|
/>
|
||||||
|
|
@ -371,34 +424,57 @@ export default function UserForm({ userId, mode }: UserFormProps) {
|
||||||
<div className="bg-white p-6 rounded-lg border">
|
<div className="bg-white p-6 rounded-lg border">
|
||||||
<h2 className="text-lg font-semibold mb-4">Riwayat Pendidikan</h2>
|
<h2 className="text-lg font-semibold mb-4">Riwayat Pendidikan</h2>
|
||||||
{educationList.map((education, index) => (
|
{educationList.map((education, index) => (
|
||||||
<div key={index} className="grid grid-cols-1 md:grid-cols-5 gap-4 mb-4">
|
<div
|
||||||
|
key={index}
|
||||||
|
className="grid grid-cols-1 md:grid-cols-5 gap-4 mb-4"
|
||||||
|
>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Masukkan Nama Sekolah/Universitas"
|
placeholder="Masukkan Nama Sekolah/Universitas"
|
||||||
value={education.schoolName}
|
value={education.schoolName}
|
||||||
onChange={(e) => handleEducationChange(index, "schoolName", e.target.value)}
|
onChange={(e) =>
|
||||||
|
handleEducationChange(index, "schoolName", e.target.value)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Masukkan Jurusan"
|
placeholder="Masukkan Jurusan"
|
||||||
value={education.major}
|
value={education.major}
|
||||||
onChange={(e) => handleEducationChange(index, "major", e.target.value)}
|
onChange={(e) =>
|
||||||
|
handleEducationChange(index, "major", e.target.value)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Masukkan Tingkat Pendidikan"
|
placeholder="Masukkan Tingkat Pendidikan"
|
||||||
value={education.educationLevel}
|
value={education.educationLevel}
|
||||||
onChange={(e) => handleEducationChange(index, "educationLevel", e.target.value)}
|
onChange={(e) =>
|
||||||
|
handleEducationChange(
|
||||||
|
index,
|
||||||
|
"educationLevel",
|
||||||
|
e.target.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Masukkan Tahun Lulus"
|
placeholder="Masukkan Tahun Lulus"
|
||||||
type="number"
|
type="number"
|
||||||
value={education.graduationYear}
|
value={education.graduationYear}
|
||||||
onChange={(e) => handleEducationChange(index, "graduationYear", e.target.value)}
|
onChange={(e) =>
|
||||||
|
handleEducationChange(
|
||||||
|
index,
|
||||||
|
"graduationYear",
|
||||||
|
e.target.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button variant="secondary" className="whitespace-nowrap">
|
<Button variant="secondary" className="whitespace-nowrap">
|
||||||
+ Ijazah
|
+ Ijazah
|
||||||
</Button>
|
</Button>
|
||||||
{index === educationList.length - 1 && (
|
{index === educationList.length - 1 && (
|
||||||
<Button type="button" variant="outline" onClick={handleAddEducation}>
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleAddEducation}
|
||||||
|
>
|
||||||
+ Tambah
|
+ Tambah
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
@ -413,31 +489,46 @@ export default function UserForm({ userId, mode }: UserFormProps) {
|
||||||
<div className="bg-white p-6 rounded-lg border">
|
<div className="bg-white p-6 rounded-lg border">
|
||||||
<h2 className="text-lg font-semibold mb-4">Riwayat Pekerjaan</h2>
|
<h2 className="text-lg font-semibold mb-4">Riwayat Pekerjaan</h2>
|
||||||
{workList.map((work, index) => (
|
{workList.map((work, index) => (
|
||||||
<div key={index} className="grid grid-cols-1 md:grid-cols-5 gap-4 mb-4">
|
<div
|
||||||
|
key={index}
|
||||||
|
className="grid grid-cols-1 md:grid-cols-5 gap-4 mb-4"
|
||||||
|
>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Masukkan Jabatan"
|
placeholder="Masukkan Jabatan"
|
||||||
value={work.jobTitle}
|
value={work.jobTitle}
|
||||||
onChange={(e) => handleWorkChange(index, "jobTitle", e.target.value)}
|
onChange={(e) =>
|
||||||
|
handleWorkChange(index, "jobTitle", e.target.value)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Masukkan Nama Perusahaan"
|
placeholder="Masukkan Nama Perusahaan"
|
||||||
value={work.companyName}
|
value={work.companyName}
|
||||||
onChange={(e) => handleWorkChange(index, "companyName", e.target.value)}
|
onChange={(e) =>
|
||||||
|
handleWorkChange(index, "companyName", e.target.value)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Tanggal Mulai"
|
placeholder="Tanggal Mulai"
|
||||||
type="date"
|
type="date"
|
||||||
value={work.startDate}
|
value={work.startDate}
|
||||||
onChange={(e) => handleWorkChange(index, "startDate", e.target.value)}
|
onChange={(e) =>
|
||||||
|
handleWorkChange(index, "startDate", e.target.value)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Tanggal Selesai"
|
placeholder="Tanggal Selesai"
|
||||||
type="date"
|
type="date"
|
||||||
value={work.endDate}
|
value={work.endDate}
|
||||||
onChange={(e) => handleWorkChange(index, "endDate", e.target.value)}
|
onChange={(e) =>
|
||||||
|
handleWorkChange(index, "endDate", e.target.value)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
{index === workList.length - 1 && (
|
{index === workList.length - 1 && (
|
||||||
<Button type="button" variant="outline" onClick={handleAddWork}>
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleAddWork}
|
||||||
|
>
|
||||||
+ Tambah
|
+ Tambah
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
@ -448,18 +539,15 @@ export default function UserForm({ userId, mode }: UserFormProps) {
|
||||||
|
|
||||||
{/* Submit Button */}
|
{/* Submit Button */}
|
||||||
<div className="flex justify-end space-x-4">
|
<div className="flex justify-end space-x-4">
|
||||||
<Button
|
<Button type="button" variant="outline" onClick={() => router.back()}>
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => router.back()}
|
|
||||||
>
|
|
||||||
Batal
|
Batal
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button type="submit" disabled={loading}>
|
||||||
type="submit"
|
{loading
|
||||||
disabled={loading}
|
? "Menyimpan..."
|
||||||
>
|
: mode === "create"
|
||||||
{loading ? "Menyimpan..." : mode === "create" ? "Buat User" : "Perbarui User"}
|
? "Buat User"
|
||||||
|
: "Perbarui User"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,16 @@ export default function Navbar(props: {
|
||||||
getNotifications();
|
getNotifications();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [hasMounted, setHasMounted] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setHasMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!hasMounted) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex justify-between items-center h-8 lg:text-2xl mt-2 lg:mt-0">
|
<div className="w-full flex justify-between items-center h-8 lg:text-2xl mt-2 lg:mt-0">
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ export default function Sidebar() {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const roleId = getCookiesDecrypt("urie");
|
const roleId = getCookiesDecrypt("urie");
|
||||||
|
const ulne = getCookiesDecrypt("ulne");
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const sidebarCollapsed = Cookies.get("sidebar");
|
const sidebarCollapsed = Cookies.get("sidebar");
|
||||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||||
|
|
@ -85,7 +86,7 @@ export default function Sidebar() {
|
||||||
const date = new Date(timestamp);
|
const date = new Date(timestamp);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const diffInHours = Math.floor(
|
const diffInHours = Math.floor(
|
||||||
(now.getTime() - date.getTime()) / (1000 * 60 * 60)
|
(now.getTime() - date.getTime()) / (1000 * 60 * 60),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (diffInHours < 1) return "Baru saja";
|
if (diffInHours < 1) return "Baru saja";
|
||||||
|
|
@ -97,6 +98,16 @@ export default function Sidebar() {
|
||||||
// Get recent sessions (last 3)
|
// Get recent sessions (last 3)
|
||||||
const recentSessions = sessions?.slice(0, 3);
|
const recentSessions = sessions?.slice(0, 3);
|
||||||
|
|
||||||
|
const [hasMounted, setHasMounted] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setHasMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!hasMounted) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<aside
|
<aside
|
||||||
|
|
@ -413,181 +424,8 @@ export default function Sidebar() {
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{roleId === "3" && (
|
{roleId === "3" &&
|
||||||
<>
|
(ulne == "2" ? (
|
||||||
<Link href={"/admin/menu"}>
|
|
||||||
<button
|
|
||||||
className={`flex items-center w-full gap-2 px-3 py-2 rounded ${
|
|
||||||
pathname.includes("/admin/menu") ? "bg-gray-200" : ""
|
|
||||||
} hover:bg-gray-300 cursor-pointer`}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
className="w-[18px] lg:w-[24px]"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill="currentColor"
|
|
||||||
d="M4 4h4v4H4zm0 6h4v4H4zm4 6H4v4h4zm2-12h4v4h-4zm4 6h-4v4h4zm-4 6h4v4h-4zM20 4h-4v4h4zm-4 6h4v4h-4zm4 6h-4v4h4z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
{!isCollapsed && <span>Menu</span>}
|
|
||||||
</button>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<details className="group mb-0">
|
|
||||||
<summary
|
|
||||||
className={`flex items-center w-full gap-2 px-3 py-2 rounded ${
|
|
||||||
pathname.includes("/admin/history") ? "bg-gray-200" : ""
|
|
||||||
} hover:bg-gray-300 cursor-pointer`}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 256 256"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill="currentColor"
|
|
||||||
d="M244 56v64a12 12 0 0 1-24 0V85l-75.51 75.52a12 12 0 0 1-17 0L96 129l-63.51 63.49a12 12 0 0 1-17-17l72-72a12 12 0 0 1 17 0L136 135l67-67h-35a12 12 0 0 1 0-24h64a12 12 0 0 1 12 12"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
{!isCollapsed && <span>Improvement</span>}
|
|
||||||
|
|
||||||
{!isCollapsed && (
|
|
||||||
<div className="flex ml-auto items-center gap-2">
|
|
||||||
<ChevronDown
|
|
||||||
className="transition-transform group-open:rotate-180"
|
|
||||||
size={14}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</summary>
|
|
||||||
|
|
||||||
{!isCollapsed && (
|
|
||||||
<div className="ml-6 mt-2 space-y-2 text-gray-500 flex flex-col">
|
|
||||||
<Link
|
|
||||||
href="/admin/dashboard"
|
|
||||||
className="hover:text-gray-700 transition-colors font-medium text-xs lg:text-base"
|
|
||||||
>
|
|
||||||
New Chat
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<details className="group/history">
|
|
||||||
<summary className="flex items-center justify-between cursor-pointer hover:text-gray-700 transition-colors font-medium text-xs lg:text-base">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span>History ({sessions.length})</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<button
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
fetchSessions();
|
|
||||||
}}
|
|
||||||
disabled={loading}
|
|
||||||
className="p-1 hover:bg-gray-300 rounded transition-colors"
|
|
||||||
title="Refresh history"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`w-3 h-3 ${
|
|
||||||
loading ? "animate-spin" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="none"
|
|
||||||
className="w-full h-full"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M1 4v6h6M23 20v-6h-6"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<ChevronDown
|
|
||||||
size={14}
|
|
||||||
className="transition-transform group-open/history:rotate-180"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</summary>
|
|
||||||
|
|
||||||
<div className="mt-2 space-y-2 pl-2">
|
|
||||||
{loading ? (
|
|
||||||
<div className="flex items-center gap-2 text-xs">
|
|
||||||
<div className="animate-spin rounded-full h-3 w-3 border-b border-blue-600"></div>
|
|
||||||
<span>Memuat...</span>
|
|
||||||
</div>
|
|
||||||
) : recentSessions.length > 0 ? (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="text-xs font-medium text-gray-400 uppercase tracking-wide">
|
|
||||||
Terbaru
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{recentSessions.map((session) => (
|
|
||||||
<Link
|
|
||||||
key={session.id}
|
|
||||||
href={`/admin/history/detail/${session.id}`}
|
|
||||||
className="block rounded transition-colors"
|
|
||||||
>
|
|
||||||
<div className="flex items-start gap-2">
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="text-sm font-medium text-gray-500 hover:text-gray-700 line-clamp-1">
|
|
||||||
{session.title}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-xs text-gray-400 mt-1">
|
|
||||||
<span>
|
|
||||||
{formatRelativeTime(session.createdAt)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="text-xs text-gray-400">
|
|
||||||
Belum ada riwayat chat
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<Link href={"/admin/schedule"}>
|
|
||||||
<button
|
|
||||||
className={`flex items-center w-full gap-2 px-3 py-2 rounded ${
|
|
||||||
pathname.includes("/admin/schedule") ? "bg-gray-200" : ""
|
|
||||||
} hover:bg-gray-300 cursor-pointer`}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
className="w-[18px] lg:w-[24px]"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill="currentColor"
|
|
||||||
d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2M12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8s8 3.58 8 8s-3.58 8-8 8m.5-13H11v6l5.25 3.15l.75-1.23l-4.5-2.67z"
|
|
||||||
/>
|
|
||||||
</svg>{" "}
|
|
||||||
{!isCollapsed && <span>Jadwal</span>}
|
|
||||||
</button>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<Link href={"/admin/data-knowledge"}>
|
<Link href={"/admin/data-knowledge"}>
|
||||||
<button
|
<button
|
||||||
className={`flex items-center w-full gap-2 px-3 py-2 rounded ${
|
className={`flex items-center w-full gap-2 px-3 py-2 rounded ${
|
||||||
|
|
@ -618,40 +456,304 @@ export default function Sidebar() {
|
||||||
{!isCollapsed && <span>Data Knowledge</span>}
|
{!isCollapsed && <span>Data Knowledge</span>}
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href={"/admin/user-memory"}>
|
) : (
|
||||||
<button
|
<>
|
||||||
className={`flex items-center w-full gap-2 px-3 py-2 rounded ${
|
<Link href={"/admin/menu"}>
|
||||||
pathname.includes("/admin/user-memory") ? "bg-gray-200" : ""
|
<button
|
||||||
} hover:bg-gray-300 cursor-pointer`}
|
className={`flex items-center w-full gap-2 px-3 py-2 rounded ${
|
||||||
>
|
pathname.includes("/admin/menu") ? "bg-gray-200" : ""
|
||||||
<svg
|
} hover:bg-gray-300 cursor-pointer`}
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
className="w-[18px] lg:w-[24px]"
|
|
||||||
>
|
>
|
||||||
<g
|
<svg
|
||||||
fill="none"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
stroke="currentColor"
|
width="24"
|
||||||
strokeLinecap="round"
|
height="24"
|
||||||
strokeLinejoin="round"
|
viewBox="0 0 24 24"
|
||||||
strokeWidth="2"
|
className="w-[18px] lg:w-[24px]"
|
||||||
>
|
>
|
||||||
<path d="M12 18V5m3 8a4.17 4.17 0 0 1-3-4a4.17 4.17 0 0 1-3 4m8.598-6.5A3 3 0 1 0 12 5a3 3 0 1 0-5.598 1.5" />
|
<path
|
||||||
<path d="M17.997 5.125a4 4 0 0 1 2.526 5.77" />
|
fill="currentColor"
|
||||||
<path d="M18 18a4 4 0 0 0 2-7.464" />
|
d="M4 4h4v4H4zm0 6h4v4H4zm4 6H4v4h4zm2-12h4v4h-4zm4 6h-4v4h4zm-4 6h4v4h-4zM20 4h-4v4h4zm-4 6h4v4h-4zm4 6h-4v4h4z"
|
||||||
<path d="M19.967 17.483A4 4 0 1 1 12 18a4 4 0 1 1-7.967-.517" />
|
/>
|
||||||
<path d="M6 18a4 4 0 0 1-2-7.464" />
|
</svg>
|
||||||
<path d="M6.003 5.125a4 4 0 0 0-2.526 5.77" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
{!isCollapsed && <span>User Memory</span>}
|
{!isCollapsed && <span>Menu</span>}
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
|
||||||
)}
|
<details className="group mb-0">
|
||||||
|
<summary
|
||||||
|
className={`flex items-center w-full gap-2 px-3 py-2 rounded ${
|
||||||
|
pathname.includes("/admin/history") ? "bg-gray-200" : ""
|
||||||
|
} hover:bg-gray-300 cursor-pointer`}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 256 256"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M244 56v64a12 12 0 0 1-24 0V85l-75.51 75.52a12 12 0 0 1-17 0L96 129l-63.51 63.49a12 12 0 0 1-17-17l72-72a12 12 0 0 1 17 0L136 135l67-67h-35a12 12 0 0 1 0-24h64a12 12 0 0 1 12 12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
{!isCollapsed && <span>Improvement</span>}
|
||||||
|
|
||||||
|
{!isCollapsed && (
|
||||||
|
<div className="flex ml-auto items-center gap-2">
|
||||||
|
<ChevronDown
|
||||||
|
className="transition-transform group-open:rotate-180"
|
||||||
|
size={14}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</summary>
|
||||||
|
|
||||||
|
{!isCollapsed && (
|
||||||
|
<div className="ml-6 mt-2 space-y-2 text-gray-500 flex flex-col">
|
||||||
|
<Link
|
||||||
|
href="/admin/dashboard"
|
||||||
|
className="hover:text-gray-700 transition-colors font-medium text-xs lg:text-base"
|
||||||
|
>
|
||||||
|
New Chat
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<details className="group/history">
|
||||||
|
<summary className="flex items-center justify-between cursor-pointer hover:text-gray-700 transition-colors font-medium text-xs lg:text-base">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span>History ({sessions.length})</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
fetchSessions();
|
||||||
|
}}
|
||||||
|
disabled={loading}
|
||||||
|
className="p-1 hover:bg-gray-300 rounded transition-colors"
|
||||||
|
title="Refresh history"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`w-3 h-3 ${
|
||||||
|
loading ? "animate-spin" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
className="w-full h-full"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M1 4v6h6M23 20v-6h-6"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<ChevronDown
|
||||||
|
size={14}
|
||||||
|
className="transition-transform group-open/history:rotate-180"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</summary>
|
||||||
|
|
||||||
|
<div className="mt-2 space-y-2 pl-2">
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex items-center gap-2 text-xs">
|
||||||
|
<div className="animate-spin rounded-full h-3 w-3 border-b border-blue-600"></div>
|
||||||
|
<span>Memuat...</span>
|
||||||
|
</div>
|
||||||
|
) : recentSessions.length > 0 ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-xs font-medium text-gray-400 uppercase tracking-wide">
|
||||||
|
Terbaru
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{recentSessions.map((session) => (
|
||||||
|
<Link
|
||||||
|
key={session.id}
|
||||||
|
href={`/admin/history/detail/${session.id}`}
|
||||||
|
className="block rounded transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="text-sm font-medium text-gray-500 hover:text-gray-700 line-clamp-1">
|
||||||
|
{session.title}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-xs text-gray-400 mt-1">
|
||||||
|
<span>
|
||||||
|
{formatRelativeTime(
|
||||||
|
session.createdAt,
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-xs text-gray-400">
|
||||||
|
Belum ada riwayat chat
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</details>
|
||||||
|
{ulne == "3" && (
|
||||||
|
<Link href={"/admin/profile"}>
|
||||||
|
<button
|
||||||
|
className={`flex items-center w-full gap-2 px-3 py-2 rounded ${
|
||||||
|
pathname.includes("/admin/profile") ? "bg-gray-200" : ""
|
||||||
|
} hover:bg-gray-300 cursor-pointer`}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
className="w-[18px] lg:w-[24px]"
|
||||||
|
>
|
||||||
|
<g fill="none">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 18a4 4 0 0 1 4-4h8a4 4 0 0 1 4 4a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2"
|
||||||
|
opacity="0.16"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
d="M4 18a4 4 0 0 1 4-4h8a4 4 0 0 1 4 4a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2Z"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="12"
|
||||||
|
cy="7"
|
||||||
|
r="3"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
{!isCollapsed && <span>Profile</span>}
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
{ulne == "2" && (
|
||||||
|
<Link href={"/admin/agents"}>
|
||||||
|
<button
|
||||||
|
className={`flex items-center w-full gap-2 px-3 py-2 rounded ${
|
||||||
|
pathname.includes("/admin/agents") ? "bg-gray-200" : ""
|
||||||
|
} hover:bg-gray-300 cursor-pointer`}
|
||||||
|
>
|
||||||
|
<Layers size={24} />
|
||||||
|
{!isCollapsed && <span>Tenaga Ahli</span>}
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{ulne == "3" && (
|
||||||
|
<Link href={"/admin/schedule"}>
|
||||||
|
<button
|
||||||
|
className={`flex items-center w-full gap-2 px-3 py-2 rounded ${
|
||||||
|
pathname.includes("/admin/schedule")
|
||||||
|
? "bg-gray-200"
|
||||||
|
: ""
|
||||||
|
} hover:bg-gray-300 cursor-pointer`}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
className="w-[18px] lg:w-[24px]"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2M12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8s8 3.58 8 8s-3.58 8-8 8m.5-13H11v6l5.25 3.15l.75-1.23l-4.5-2.67z"
|
||||||
|
/>
|
||||||
|
</svg>{" "}
|
||||||
|
{!isCollapsed && <span>Jadwal</span>}
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Link href={"/admin/data-knowledge"}>
|
||||||
|
<button
|
||||||
|
className={`flex items-center w-full gap-2 px-3 py-2 rounded ${
|
||||||
|
pathname.includes("/admin/data-knowledge")
|
||||||
|
? "bg-gray-200"
|
||||||
|
: ""
|
||||||
|
} hover:bg-gray-300 cursor-pointer`}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 48 48"
|
||||||
|
className="w-[18px] lg:w-[24px]"
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2.5"
|
||||||
|
>
|
||||||
|
<path d="M44 11v27c0 3.314-8.954 6-20 6S4 41.314 4 38V11" />
|
||||||
|
<path d="M44 29c0 3.314-8.954 6-20 6S4 32.314 4 29m40-9c0 3.314-8.954 6-20 6S4 23.314 4 20" />
|
||||||
|
<ellipse cx="24" cy="10" rx="20" ry="6" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
{!isCollapsed && <span>Data Knowledge</span>}
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
<Link href={"/admin/user-memory"}>
|
||||||
|
<button
|
||||||
|
className={`flex items-center w-full gap-2 px-3 py-2 rounded ${
|
||||||
|
pathname.includes("/admin/user-memory")
|
||||||
|
? "bg-gray-200"
|
||||||
|
: ""
|
||||||
|
} hover:bg-gray-300 cursor-pointer`}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
className="w-[18px] lg:w-[24px]"
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<path d="M12 18V5m3 8a4.17 4.17 0 0 1-3-4a4.17 4.17 0 0 1-3 4m8.598-6.5A3 3 0 1 0 12 5a3 3 0 1 0-5.598 1.5" />
|
||||||
|
<path d="M17.997 5.125a4 4 0 0 1 2.526 5.77" />
|
||||||
|
<path d="M18 18a4 4 0 0 0 2-7.464" />
|
||||||
|
<path d="M19.967 17.483A4 4 0 1 1 12 18a4 4 0 1 1-7.967-.517" />
|
||||||
|
<path d="M6 18a4 4 0 0 1-2-7.464" />
|
||||||
|
<path d="M6.003 5.125a4 4 0 0 0-2.526 5.77" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
{!isCollapsed && <span>User Memory</span>}
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
))}
|
||||||
{roleId === "2" && (
|
{roleId === "2" && (
|
||||||
<>
|
<>
|
||||||
<Link href={"/admin/dashboard"}>
|
<Link href={"/admin/dashboard"}>
|
||||||
|
|
@ -834,6 +936,16 @@ export default function Sidebar() {
|
||||||
{!isCollapsed && <span>Manajemen User</span>}
|
{!isCollapsed && <span>Manajemen User</span>}
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link href={"/admin/agents"}>
|
||||||
|
<button
|
||||||
|
className={`flex items-center w-full gap-2 px-3 py-2 rounded ${
|
||||||
|
pathname.includes("/admin/agents") ? "bg-gray-200" : ""
|
||||||
|
} hover:bg-gray-300 cursor-pointer`}
|
||||||
|
>
|
||||||
|
<Layers size={24} />
|
||||||
|
{!isCollapsed && <span>Tenaga Ahli</span>}
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
<Link href={"/admin/management-ebook"}>
|
<Link href={"/admin/management-ebook"}>
|
||||||
<button
|
<button
|
||||||
className={`flex items-center w-full gap-2 px-3 py-2 rounded ${
|
className={`flex items-center w-full gap-2 px-3 py-2 rounded ${
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,110 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useMemo } from "react";
|
||||||
import { Input } from "../ui/input";
|
import { Input } from "../ui/input";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import { BellIcon, Plus } from "lucide-react";
|
import { BellIcon, Plus } from "lucide-react";
|
||||||
import { getAllUsers } from "@/service/user";
|
import { getAllUsers, getUserExpert, saveUserExpert } from "@/service/user";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
|
import Navbar from "../navbar";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "../ui/dialog";
|
||||||
|
import { getAllAgent } from "@/service/agent";
|
||||||
|
import { close, error, loading } from "@/config/swal";
|
||||||
|
import { Checkbox } from "../ui/checkbox";
|
||||||
|
import CustomPagination from "../custom-pagination";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "../ui/select";
|
||||||
|
import Swal from "sweetalert2";
|
||||||
|
import withReactContent from "sweetalert2-react-content";
|
||||||
|
|
||||||
type User = {
|
type User = {
|
||||||
id: number;
|
id: number;
|
||||||
fullname: string;
|
fullname: string;
|
||||||
|
username: string;
|
||||||
email: string;
|
email: string;
|
||||||
user_role_id: number;
|
userRoleId: number;
|
||||||
created_at: string;
|
userLevelId: number;
|
||||||
is_active: boolean;
|
createdAt: string;
|
||||||
|
isActive: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface AgentData {
|
||||||
|
id: number;
|
||||||
|
agentId: string;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{ label: "Semua", value: "0" },
|
||||||
|
{ label: "Admin", value: "1" },
|
||||||
|
{ label: "Tenaga Ahli", value: "3" },
|
||||||
|
{ label: "Pengguna Umum", value: "4" },
|
||||||
|
];
|
||||||
|
|
||||||
export default function ManajemenUser() {
|
export default function ManajemenUser() {
|
||||||
const [mounted, setMounted] = useState(false);
|
|
||||||
const [users, setUsers] = useState<User[]>([]);
|
const [users, setUsers] = useState<User[]>([]);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [loading, setLoading] = useState(true);
|
const [searchAgent, setSearchAgent] = useState("");
|
||||||
|
const [agents, setAgents] = useState<AgentData[]>([]);
|
||||||
|
const [selectedAgents, setSelectedAgents] = useState<string[]>([]);
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [limit, setLimit] = useState(5);
|
||||||
|
const [totalData, setTotalData] = useState(0);
|
||||||
|
const [totalPage, setTotalPage] = useState(1);
|
||||||
|
const [userType, setUserType] = useState("0");
|
||||||
|
const MySwal = withReactContent(Swal);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true);
|
initFetch();
|
||||||
loadUsers();
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const initFetch = async () => {
|
||||||
|
loading();
|
||||||
|
// const req = { page: page, title: search, limit: limit, createdById: "" };
|
||||||
|
const res = await getAllAgent();
|
||||||
|
// setTotalData(res?.meta?.count || 0);
|
||||||
|
setAgents(res?.data?.data || []);
|
||||||
|
close();
|
||||||
|
};
|
||||||
|
const filteredExperts = useMemo(() => {
|
||||||
|
const filteredExperts = agents.filter((agent) =>
|
||||||
|
agent.name?.toLowerCase().includes(searchAgent.toLowerCase()),
|
||||||
|
);
|
||||||
|
return filteredExperts ? filteredExperts : [];
|
||||||
|
}, [searchAgent, agents]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadUsers();
|
||||||
|
}, [page, limit, userType]);
|
||||||
|
|
||||||
const loadUsers = async () => {
|
const loadUsers = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
loading(true);
|
||||||
const response = await getAllUsers();
|
const response = await getAllUsers({
|
||||||
|
limit: limit,
|
||||||
|
page: page,
|
||||||
|
search: search,
|
||||||
|
userRoleId: userType == "0" ? "" : userType,
|
||||||
|
});
|
||||||
if (!response.error && response.data?.data) {
|
if (!response.error && response.data?.data) {
|
||||||
|
setTotalData(response?.data?.meta?.count);
|
||||||
|
setTotalPage(response?.data?.meta?.totalPage);
|
||||||
setUsers(response.data.data);
|
setUsers(response.data.data);
|
||||||
} else {
|
} else {
|
||||||
toast.error("Gagal memuat data users");
|
toast.error("Gagal memuat data users");
|
||||||
|
|
@ -40,15 +112,19 @@ export default function ManajemenUser() {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error("Terjadi kesalahan saat memuat data users");
|
toast.error("Terjadi kesalahan saat memuat data users");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
close();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUserRoleName = (roleId: number) => {
|
const getUserRoleName = (roleId: number, level?: number) => {
|
||||||
switch (roleId) {
|
switch (roleId) {
|
||||||
|
case 1:
|
||||||
|
return "Admin";
|
||||||
case 2:
|
case 2:
|
||||||
return "Tenaga Ahli";
|
return "Tenaga Ahli";
|
||||||
case 3:
|
case 3:
|
||||||
|
return level == 2 ? "Approver Tenaga Ahli" : "Tenaga Ahli";
|
||||||
|
case 4:
|
||||||
return "Pengguna Umum";
|
return "Pengguna Umum";
|
||||||
default:
|
default:
|
||||||
return "Unknown";
|
return "Unknown";
|
||||||
|
|
@ -56,75 +132,130 @@ export default function ManajemenUser() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatDate = (dateString: string) => {
|
const formatDate = (dateString: string) => {
|
||||||
return new Date(dateString).toLocaleDateString('id-ID', {
|
return new Date(dateString).toLocaleDateString("id-ID", {
|
||||||
year: 'numeric',
|
year: "numeric",
|
||||||
month: 'long',
|
month: "long",
|
||||||
day: 'numeric',
|
day: "numeric",
|
||||||
hour: '2-digit',
|
hour: "2-digit",
|
||||||
minute: '2-digit',
|
minute: "2-digit",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredUsers = users.filter((user) =>
|
const filteredUsers = users.filter(
|
||||||
user.fullname.toLowerCase().includes(search.toLowerCase()) ||
|
(user) =>
|
||||||
user.email.toLowerCase().includes(search.toLowerCase())
|
user.fullname.toLowerCase().includes(search.toLowerCase()) ||
|
||||||
|
user.email.toLowerCase().includes(search.toLowerCase()),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!mounted) {
|
const getUserExpertData = async (id: number) => {
|
||||||
return (
|
loading();
|
||||||
<div className="flex justify-center items-center h-64">
|
const res = await getUserExpert(id);
|
||||||
<div className="text-lg">Loading...</div>
|
const data: AgentData[] = res?.data?.data.agentId ?? [];
|
||||||
</div>
|
const temp = data.map((item) => {
|
||||||
);
|
return String(item.id);
|
||||||
|
});
|
||||||
|
setSelectedAgents(temp);
|
||||||
|
close();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (checked: boolean | string, id: string) => {
|
||||||
|
setSelectedAgents((prev) => {
|
||||||
|
const isChecked = checked === true;
|
||||||
|
|
||||||
|
// jika dicentang → tambahkan
|
||||||
|
if (isChecked) {
|
||||||
|
// hindari double data
|
||||||
|
if (prev.includes(id)) return prev;
|
||||||
|
return [...prev, id];
|
||||||
|
}
|
||||||
|
|
||||||
|
// jika di-uncheck → hapus
|
||||||
|
return prev.filter((agentId) => agentId !== id);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveData = async (userId: number) => {
|
||||||
|
loading();
|
||||||
|
const req = {
|
||||||
|
user_id: userId,
|
||||||
|
agent_id: selectedAgents.map((a) => {
|
||||||
|
return Number(a);
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await saveUserExpert(req);
|
||||||
|
if (res?.error) {
|
||||||
|
error(res?.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
close();
|
||||||
|
successSubmit();
|
||||||
|
};
|
||||||
|
|
||||||
|
function successSubmit() {
|
||||||
|
MySwal.fire({
|
||||||
|
title: "Sukses",
|
||||||
|
icon: "success",
|
||||||
|
confirmButtonColor: "#3085d6",
|
||||||
|
confirmButtonText: "OK",
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
initFetch();
|
||||||
|
} else {
|
||||||
|
initFetch();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading) {
|
let typingTimer: NodeJS.Timeout;
|
||||||
return (
|
const doneTypingInterval = 1500;
|
||||||
<div className="flex justify-center items-center h-64">
|
|
||||||
<div className="text-lg">Memuat data users...</div>
|
const handleKeyUp = () => {
|
||||||
</div>
|
clearTimeout(typingTimer);
|
||||||
);
|
typingTimer = setTimeout(doneTyping, doneTypingInterval);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = () => {
|
||||||
|
clearTimeout(typingTimer);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function doneTyping() {
|
||||||
|
setPage(1);
|
||||||
|
initFetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-7xl space-y-6">
|
<div className="flex-1 overflow-hidden flex flex-col h-[90vh]">
|
||||||
<div className="flex items-center justify-between">
|
<Navbar title="Management User" />
|
||||||
<h1 className="text-lg font-semibold">Manajemen Users</h1>
|
|
||||||
<div className="flex items-center gap-4">
|
<Link href={"/admin/management-user/create"}>
|
||||||
<Link href="/admin/management-user/create">
|
<Button color="primary" className="mt-10 mb-3">
|
||||||
<Button className="flex items-center space-x-2">
|
Create User
|
||||||
<Plus className="w-4 h-4" />
|
</Button>
|
||||||
<span>Tambah User</span>
|
</Link>
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
<Input
|
|
||||||
placeholder="Search"
|
|
||||||
value={search}
|
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
|
||||||
className="w-[200px] md:w-[250px]"
|
|
||||||
/>
|
|
||||||
<button className="relative p-2 rounded-full hover:bg-gray-100">
|
|
||||||
<BellIcon className="w-5 h-5 text-gray-500" />
|
|
||||||
{/* Notifikasi badge bisa ditambahkan di sini */}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* Filter Section */}
|
|
||||||
<div className="flex flex-wrap gap-3 items-center mb-4">
|
<div className="flex flex-wrap gap-3 items-center mb-4">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Name, email, etc..."
|
placeholder="Name, email, etc..."
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
className="w-56"
|
className="w-56"
|
||||||
|
onKeyUp={handleKeyUp}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
/>
|
/>
|
||||||
<Input type="date" className="w-40" />
|
<Select value={userType} onValueChange={setUserType}>
|
||||||
<span>-</span>
|
<SelectTrigger className="w-full max-w-48">
|
||||||
<Input type="date" className="w-40" />
|
<SelectValue />
|
||||||
<select className="border rounded px-3 py-2 text-sm">
|
</SelectTrigger>
|
||||||
<option>Semua</option>
|
<SelectContent>
|
||||||
<option>Pengguna Umum</option>
|
<SelectGroup>
|
||||||
<option>Tenaga Ahli</option>
|
{items.map((item) => (
|
||||||
</select>
|
<SelectItem key={item.value} value={item.value}>
|
||||||
|
{item.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Table */}
|
{/* Table */}
|
||||||
|
|
@ -133,10 +264,11 @@ export default function ManajemenUser() {
|
||||||
<thead className="bg-gray-50 border-b">
|
<thead className="bg-gray-50 border-b">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-4 py-2">Nama Lengkap</th>
|
<th className="px-4 py-2">Nama Lengkap</th>
|
||||||
|
<th className="px-4 py-2">Username</th>
|
||||||
|
|
||||||
<th className="px-4 py-2">Email</th>
|
<th className="px-4 py-2">Email</th>
|
||||||
<th className="px-4 py-2">Jenis Akun</th>
|
<th className="px-4 py-2">Jenis Akun</th>
|
||||||
<th className="px-4 py-2">Tanggal Daftar</th>
|
<th className="px-4 py-2">Tanggal Daftar</th>
|
||||||
<th className="px-4 py-2">Status</th>
|
|
||||||
<th className="px-4 py-2">Tindakan</th>
|
<th className="px-4 py-2">Tindakan</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
@ -144,31 +276,13 @@ export default function ManajemenUser() {
|
||||||
{filteredUsers.map((user) => (
|
{filteredUsers.map((user) => (
|
||||||
<tr key={user.id} className="border-b">
|
<tr key={user.id} className="border-b">
|
||||||
<td className="px-4 py-2">{user.fullname}</td>
|
<td className="px-4 py-2">{user.fullname}</td>
|
||||||
|
<td className="px-4 py-2">{user.username}</td>
|
||||||
<td className="px-4 py-2">{user.email}</td>
|
<td className="px-4 py-2">{user.email}</td>
|
||||||
<td className="px-4 py-2">{getUserRoleName(user.user_role_id)}</td>
|
<td className="px-4 py-2">
|
||||||
<td className="px-4 py-2">{formatDate(user.created_at)}</td>
|
{getUserRoleName(user.userRoleId, user.userLevelId)}
|
||||||
<td className="px-4 py-2 flex items-center gap-2">
|
|
||||||
<span
|
|
||||||
className={!user.is_active ? "text-gray-500" : "text-gray-400"}
|
|
||||||
>
|
|
||||||
Tidak Aktif
|
|
||||||
</span>
|
|
||||||
<label className="relative inline-flex items-center cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={user.is_active}
|
|
||||||
className="sr-only peer"
|
|
||||||
readOnly
|
|
||||||
/>
|
|
||||||
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none rounded-full peer dark:bg-gray-300 peer-checked:bg-blue-500 transition"></div>
|
|
||||||
<div className="absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform peer-checked:translate-x-5"></div>
|
|
||||||
</label>
|
|
||||||
<span
|
|
||||||
className={user.is_active ? "text-gray-500" : "text-gray-400"}
|
|
||||||
>
|
|
||||||
Aktif
|
|
||||||
</span>
|
|
||||||
</td>
|
</td>
|
||||||
|
<td className="px-4 py-2">{formatDate(user.createdAt)}</td>
|
||||||
|
|
||||||
<td className="px-4 py-2 space-x-3">
|
<td className="px-4 py-2 space-x-3">
|
||||||
<Link
|
<Link
|
||||||
href={`/admin/management-user/detail/${user.id}`}
|
href={`/admin/management-user/detail/${user.id}`}
|
||||||
|
|
@ -182,6 +296,63 @@ export default function ManajemenUser() {
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</Link>
|
</Link>
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
getUserExpertData(user.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Tenaga Ahli
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-w-3xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Tenaga Ahli</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<p>Tenaga Ahli Terpilih : {selectedAgents.length}</p>
|
||||||
|
<Input
|
||||||
|
value={searchAgent}
|
||||||
|
onChange={(e) => setSearchAgent(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2 max-h-125 overflow-auto ">
|
||||||
|
{/* <p>Tenaga Ahli Terpilih : {selectedAgents.length}</p> */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-1">
|
||||||
|
{filteredExperts.map((agent) => (
|
||||||
|
<div
|
||||||
|
key={agent.id}
|
||||||
|
className="flex flex-row gap-2 items-center"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
id={String(agent.id)}
|
||||||
|
name={agent.name}
|
||||||
|
checked={selectedAgents.includes(
|
||||||
|
String(agent.id),
|
||||||
|
)}
|
||||||
|
onCheckedChange={(e) =>
|
||||||
|
handleChange(e, String(agent.id))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col ">
|
||||||
|
<p className="font-semibold">{agent.name}</p>
|
||||||
|
<p className="text-sm uppercase">
|
||||||
|
{agent.type}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter className="justify-end">
|
||||||
|
<DialogClose onClick={() => saveData(user.id)}>
|
||||||
|
<p className="text-blue-600">Save</p>
|
||||||
|
</DialogClose>
|
||||||
|
<DialogClose>
|
||||||
|
<p className="text-red-600">Close</p>
|
||||||
|
</DialogClose>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
<button className="text-red-500 hover:underline">
|
<button className="text-red-500 hover:underline">
|
||||||
Hapus
|
Hapus
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -192,20 +363,21 @@ export default function ManajemenUser() {
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
{/* Footer Pagination */}
|
{/* Footer Pagination */}
|
||||||
<div className="flex justify-between items-center px-4 py-2 text-sm">
|
<div className="flex items-center justify-end gap-2 px-4 py-2 text-sm text-gray-600 border-t">
|
||||||
<div>
|
<div className="flex items-center gap-2">
|
||||||
Rows per page:
|
Rows per page:
|
||||||
<select className="ml-2 border rounded px-1 py-0.5">
|
<select
|
||||||
<option>5</option>
|
className="border rounded-md px-2 py-1 text-sm"
|
||||||
<option>10</option>
|
value={limit}
|
||||||
<option>20</option>
|
onChange={(e) => setLimit(Number(e.target.value))}
|
||||||
|
>
|
||||||
|
<option value={5}>5</option>
|
||||||
|
<option value={10}>10</option>
|
||||||
|
<option value={20}>20</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>1–{filteredUsers.length} of {filteredUsers.length}</div>
|
|
||||||
<div className="flex gap-2">
|
<CustomPagination totalPage={totalPage} onPageChange={setPage} />
|
||||||
<button className="px-2"><</button>
|
|
||||||
<button className="px-2">></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as SwitchPrimitive from "@radix-ui/react-switch"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Switch({
|
||||||
|
className,
|
||||||
|
size = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SwitchPrimitive.Root> & {
|
||||||
|
size?: "sm" | "default"
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<SwitchPrimitive.Root
|
||||||
|
data-slot="switch"
|
||||||
|
data-size={size}
|
||||||
|
className={cn(
|
||||||
|
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 group/switch inline-flex shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-[1.15rem] data-[size=default]:w-8 data-[size=sm]:h-3.5 data-[size=sm]:w-6",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SwitchPrimitive.Thumb
|
||||||
|
data-slot="switch-thumb"
|
||||||
|
className={cn(
|
||||||
|
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block rounded-full ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SwitchPrimitive.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Switch }
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
"@radix-ui/react-radio-group": "^1.3.7",
|
"@radix-ui/react-radio-group": "^1.3.7",
|
||||||
"@radix-ui/react-select": "^2.2.5",
|
"@radix-ui/react-select": "^2.2.5",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
"@radix-ui/react-tabs": "^1.1.13",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"@ts-stack/markdown": "^1.5.0",
|
"@ts-stack/markdown": "^1.5.0",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
|
|
@ -1511,6 +1512,34 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-switch": {
|
||||||
|
"version": "1.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz",
|
||||||
|
"integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.3",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||||
|
"@radix-ui/react-use-previous": "1.1.1",
|
||||||
|
"@radix-ui/react-use-size": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-tabs": {
|
"node_modules/@radix-ui/react-tabs": {
|
||||||
"version": "1.1.13",
|
"version": "1.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz",
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@
|
||||||
"@radix-ui/react-radio-group": "^1.3.7",
|
"@radix-ui/react-radio-group": "^1.3.7",
|
||||||
"@radix-ui/react-select": "^2.2.5",
|
"@radix-ui/react-select": "^2.2.5",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
"@radix-ui/react-tabs": "^1.1.13",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"@ts-stack/markdown": "^1.5.0",
|
"@ts-stack/markdown": "^1.5.0",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
import {
|
||||||
|
httpGetInterceptor,
|
||||||
|
httpPostInterceptor,
|
||||||
|
httpPutInterceptor,
|
||||||
|
httpPatchInterceptor,
|
||||||
|
httpDeleteInterceptor,
|
||||||
|
} from "./http-config/http-interceptor-services";
|
||||||
|
|
||||||
|
import { httpGet } from "./http-config/http-base-services";
|
||||||
|
|
||||||
|
export async function createAgentData(agentData: any) {
|
||||||
|
const response = await httpPostInterceptor("/agent", agentData);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAllAgent() {
|
||||||
|
const response = await httpGetInterceptor("/agent");
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAgentById(id: string) {
|
||||||
|
const response = await httpGetInterceptor(`/agent/${id}`);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
export async function updateAgentById(data: any, id: string) {
|
||||||
|
const response = await httpPutInterceptor(`/agent/${id}`, data);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAgentByAgentId(id: string) {
|
||||||
|
const response = await httpGetInterceptor(`/agent/agent_id/${id}`);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
import {
|
||||||
|
httpGetInterceptor,
|
||||||
|
httpPostInterceptor,
|
||||||
|
httpPutInterceptor,
|
||||||
|
} from "./http-config/http-interceptor-services";
|
||||||
|
|
||||||
|
export async function getUserWorkHistory(id: number) {
|
||||||
|
const response = await httpGetInterceptor(`/work-history?userId=${id}`);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
100
service/user.ts
100
service/user.ts
|
|
@ -1,4 +1,8 @@
|
||||||
import { httpGetInterceptor, httpPostInterceptor, httpPutInterceptor } from "./http-config/http-interceptor-services";
|
import {
|
||||||
|
httpGetInterceptor,
|
||||||
|
httpPostInterceptor,
|
||||||
|
httpPutInterceptor,
|
||||||
|
} from "./http-config/http-interceptor-services";
|
||||||
|
|
||||||
export interface UserData {
|
export interface UserData {
|
||||||
id?: number;
|
id?: number;
|
||||||
|
|
@ -42,30 +46,30 @@ export interface UserDetailResponse {
|
||||||
email: string;
|
email: string;
|
||||||
fullname: string;
|
fullname: string;
|
||||||
address: string;
|
address: string;
|
||||||
phone_number: string;
|
phoneNumber: string;
|
||||||
work_type: string | null;
|
workType: string | null;
|
||||||
gender_type: string;
|
genderType: string;
|
||||||
identity_type: string | null;
|
identityType: string | null;
|
||||||
identity_group: string | null;
|
identityGroup: string | null;
|
||||||
identity_group_number: string | null;
|
identityGroupNumber: string | null;
|
||||||
identity_number: string | null;
|
identityNumber: string | null;
|
||||||
date_of_birth: string;
|
dateOfBirth: string;
|
||||||
last_education: string | null;
|
lastEducation: string | null;
|
||||||
degree: string | null;
|
degree: string | null;
|
||||||
whatsapp_number: string;
|
whatsappNumber: string;
|
||||||
last_job_title: string | null;
|
lastJobTitle: string | null;
|
||||||
user_role_id: number;
|
userRoleId: number;
|
||||||
user_level_id: number;
|
userLevelId: number;
|
||||||
user_levels: any;
|
userLevels: any;
|
||||||
keycloak_id: string;
|
keycloakId: string;
|
||||||
status_id: number;
|
statusId: number;
|
||||||
created_by_id: number;
|
createdById: number;
|
||||||
profile_picture_path: string | null;
|
profilePicturePath: string | null;
|
||||||
temp_password: string;
|
tempPassword: string;
|
||||||
is_email_updated: boolean;
|
isEmailUpdated: boolean;
|
||||||
is_active: boolean;
|
isActive: boolean;
|
||||||
created_at: string;
|
createdAt: string;
|
||||||
updated_at: string;
|
updatedAt: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -88,8 +92,10 @@ export async function getUserDetail(id: number): Promise<UserDetailResponse> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all users
|
// Get all users
|
||||||
export async function getAllUsers() {
|
export async function getAllUsers(data: any) {
|
||||||
const response = await httpGetInterceptor("/users");
|
const response = await httpGetInterceptor(
|
||||||
|
`/users?limit=${data?.limit || ""}&page=${data?.page || ""}&userRoleId=${data.userRoleId}`,
|
||||||
|
);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,19 +106,53 @@ export async function createWorkHistory(workData: WorkHistoryData) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create education history
|
// Create education history
|
||||||
export async function createEducationHistory(educationData: EducationHistoryData) {
|
export async function createEducationHistory(
|
||||||
const response = await httpPostInterceptor("/education-history", educationData);
|
educationData: EducationHistoryData,
|
||||||
|
) {
|
||||||
|
const response = await httpPostInterceptor(
|
||||||
|
"/education-history",
|
||||||
|
educationData,
|
||||||
|
);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user work history
|
// Get user work history
|
||||||
export async function getUserWorkHistory(userId: number) {
|
export async function getUserWorkHistory(userId: number) {
|
||||||
const response = await httpGetInterceptor(`/work-history/user/${userId}`);
|
const response = await httpGetInterceptor(`/work-history?userId=${userId}`);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user education history
|
// Get user education history
|
||||||
export async function getUserEducationHistory(userId: number) {
|
export async function getUserEducationHistory(userId: number) {
|
||||||
const response = await httpGetInterceptor(`/education-history/user/${userId}`);
|
const response = await httpGetInterceptor(
|
||||||
|
`/education-history?userId=${userId}`,
|
||||||
|
);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUserResearchJournal(userId: number) {
|
||||||
|
const response = await httpGetInterceptor(
|
||||||
|
`/research-journals?userId=${userId}`,
|
||||||
|
);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAllExperts(name: string, type: string) {
|
||||||
|
const response = await httpGetInterceptor(
|
||||||
|
`/users/experts?name=${name}&type=${type}`,
|
||||||
|
);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUserExpert(id: number) {
|
||||||
|
const response = await httpGetInterceptor(`/user-agent?user_id=${id}`);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveUserExpert(data: {
|
||||||
|
agent_id: Number[];
|
||||||
|
user_id: number;
|
||||||
|
}) {
|
||||||
|
const response = await httpPutInterceptor(`/user-agent`, data);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -192,8 +192,6 @@ export const getTimeStamp = (createdAt: Date): string => {
|
||||||
(now.getTime() - createdAt.getTime()) / 1000,
|
(now.getTime() - createdAt.getTime()) / 1000,
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log("adatw", differenceInSeconds, now, createdAt);
|
|
||||||
|
|
||||||
const intervals: { [key: string]: number } = {
|
const intervals: { [key: string]: number } = {
|
||||||
year: 31536000,
|
year: 31536000,
|
||||||
month: 2592000,
|
month: 2592000,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue