Compare commits

...

10 Commits

Author SHA1 Message Date
Rama Priyanto 8ce5d3e8dc feat:update agent 2026-02-12 15:09:54 +07:00
Rama Priyanto 8e88205bf0 fix:error message mapping agent, feat:agent edit 2026-02-09 16:05:46 +07:00
Rama Priyanto 51769608d7 fix:user layout 2026-02-09 09:54:28 +07:00
Rama Priyanto 55c2c10c51 fix:user layout 2026-02-09 08:08:46 +07:00
Rama Priyanto c44238b468 fix:mapping agent for user 2026-02-09 07:18:29 +07:00
Rama Priyanto 2b52959531 fix:expert menu 2026-02-02 03:15:28 +07:00
Rama Priyanto 1aa9a37898 fix:admin agent 2026-02-02 02:59:35 +07:00
Rama Priyanto fa3a9494bd fix:build 2026-02-02 02:25:54 +07:00
Rama Priyanto f10b38d020 fix:agent list 2026-02-02 02:15:19 +07:00
Rama Priyanto bd26a0446d fix:edit profile tenaga ahli 2026-01-30 18:50:59 +07:00
24 changed files with 2076 additions and 675 deletions

View File

@ -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>
);
}

View File

@ -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>
); );

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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"

View File

@ -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);
} }

View File

@ -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>

View File

@ -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>
))} ))}

View File

@ -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]);

View File

@ -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");
}
} }
}; };

View File

@ -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,

View File

@ -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>

View File

@ -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>

View File

@ -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">

View File

@ -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 ${

View File

@ -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">&lt;</button>
<button className="px-2">&gt;</button>
</div>
</div> </div>
</div> </div>
</div> </div>

35
components/ui/switch.tsx Normal file
View File

@ -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 }

29
package-lock.json generated
View File

@ -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",

View File

@ -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",

33
service/agent.tsx Normal file
View File

@ -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;
}

10
service/profle.tsx Normal file
View File

@ -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;
}

View File

@ -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;
} }

View File

@ -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,