updaye
This commit is contained in:
commit
91be4b0518
|
|
@ -17,6 +17,8 @@ import {
|
||||||
ChatSessionDetail,
|
ChatSessionDetail,
|
||||||
ChatMessage,
|
ChatMessage,
|
||||||
} from "@/lib/chatHistory";
|
} from "@/lib/chatHistory";
|
||||||
|
import Cookies from "js-cookie";
|
||||||
|
const token = Cookies.get("access_token");
|
||||||
|
|
||||||
interface ExternalChatRun {
|
interface ExternalChatRun {
|
||||||
message: {
|
message: {
|
||||||
|
|
@ -35,28 +37,37 @@ interface ExternalChatRun {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ExternalChatResponse {
|
// interface ExternalChatResponse {
|
||||||
session_id: string;
|
// sessionId: string;
|
||||||
agent_id: string;
|
// agentId: string;
|
||||||
user_id: string | null;
|
// userId: string | null;
|
||||||
runs: ExternalChatRun[];
|
// runs: ExternalChatRun[];
|
||||||
memory: {
|
// memory: {
|
||||||
runs: ExternalChatRun[];
|
// runs: ExternalChatRun[];
|
||||||
chats: ExternalChatRun[];
|
// chats: ExternalChatRun[];
|
||||||
};
|
// };
|
||||||
agent_data: {
|
// agent_data: {
|
||||||
session_type: string;
|
// session_type: string;
|
||||||
status: string;
|
// status: string;
|
||||||
metadata: any;
|
// metadata: any;
|
||||||
};
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
|
interface HistoryData {
|
||||||
|
id: number;
|
||||||
|
sessionId: string;
|
||||||
|
messageType: string;
|
||||||
|
content: string;
|
||||||
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function HistoryDetailPage() {
|
export default function HistoryDetailPage() {
|
||||||
const [sessionDetail, setSessionDetail] = useState<ChatSessionDetail | null>(
|
const [sessionDetail, setSessionDetail] = useState<ChatSessionDetail | null>(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
const [externalChatData, setExternalChatData] =
|
const [externalChatData, setExternalChatData] = useState<
|
||||||
useState<ExternalChatResponse | null>(null);
|
HistoryData[] | null
|
||||||
|
>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [loadingExternal, setLoadingExternal] = useState(false);
|
const [loadingExternal, setLoadingExternal] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
@ -77,12 +88,12 @@ export default function HistoryDetailPage() {
|
||||||
try {
|
try {
|
||||||
// Get session details from local API
|
// Get session details from local API
|
||||||
const response = await getSessionDetail(chatId);
|
const response = await getSessionDetail(chatId);
|
||||||
setSessionDetail(response.session);
|
setSessionDetail(response.data);
|
||||||
|
|
||||||
// Fetch external chat data
|
// Fetch external chat data
|
||||||
await fetchExternalChatData(
|
await fetchExternalChatData(
|
||||||
response.session.agent_id,
|
response.data.agentId,
|
||||||
response.session.session_id
|
response.data.sessionId
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching session details:", error);
|
console.error("Error fetching session details:", error);
|
||||||
|
|
@ -96,16 +107,27 @@ export default function HistoryDetailPage() {
|
||||||
setLoadingExternal(true);
|
setLoadingExternal(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// const response = await fetch(
|
||||||
|
// `https://narasiahli.com/ai/api/v1/agents/${agentId}/sessions/${sessionId}`
|
||||||
|
// );
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`https://narasiahli.com/ai/api/v1/agents/${agentId}/sessions/${sessionId}`
|
`https://narasiahli.com/api/ai-chat/sessions/${chatId}/messages`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data: ExternalChatResponse = await response.json();
|
const data = await response.json();
|
||||||
setExternalChatData(data);
|
setExternalChatData(data.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching external chat data:", error);
|
console.error("Error fetching external chat data:", error);
|
||||||
setError("Gagal memuat data chat dari server eksternal");
|
setError("Gagal memuat data chat dari server eksternal");
|
||||||
|
|
@ -137,7 +159,7 @@ export default function HistoryDetailPage() {
|
||||||
|
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
if (sessionDetail) {
|
if (sessionDetail) {
|
||||||
fetchExternalChatData(sessionDetail.agent_id, sessionDetail.session_id);
|
fetchExternalChatData(sessionDetail.agentId, sessionDetail.sessionId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -216,7 +238,7 @@ export default function HistoryDetailPage() {
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Clock className="w-4 h-4 text-gray-500" />
|
<Clock className="w-4 h-4 text-gray-500" />
|
||||||
<span className="text-gray-600">
|
<span className="text-gray-600">
|
||||||
Dibuat: {formatDate(sessionDetail.created_at)}
|
Dibuat: {formatDate(sessionDetail.createdAt)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{/* <div className="flex items-center gap-2">
|
{/* <div className="flex items-center gap-2">
|
||||||
|
|
@ -226,7 +248,7 @@ export default function HistoryDetailPage() {
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<User className="w-4 h-4 text-gray-500" />
|
<User className="w-4 h-4 text-gray-500" />
|
||||||
<span className="text-gray-600">
|
<span className="text-gray-600">
|
||||||
Agent: {sessionDetail.agent_id.substring(0, 8)}...
|
Agent: {sessionDetail.agentId.substring(0, 8)}...
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -241,50 +263,50 @@ export default function HistoryDetailPage() {
|
||||||
<span>Memuat pesan chat...</span>
|
<span>Memuat pesan chat...</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : externalChatData && externalChatData.runs.length > 0 ? (
|
) : externalChatData && externalChatData.length > 0 ? (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{externalChatData.runs.map((run, index) => (
|
{externalChatData.map((run, index) => (
|
||||||
<div key={index} className="space-y-4">
|
<div key={index} className="space-y-4">
|
||||||
{/* User Message */}
|
{run.messageType === "user" ? (
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<div className="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
|
<div className="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
|
||||||
<User className="w-4 h-4 text-blue-600" />
|
<User className="w-4 h-4 text-blue-600" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="flex-1">
|
||||||
<div className="flex-1">
|
<div className="bg-white border rounded-lg p-4 shadow-sm">
|
||||||
<div className="bg-white border rounded-lg p-4 shadow-sm">
|
<p className="text-gray-900 whitespace-pre-wrap">
|
||||||
<p className="text-gray-900 whitespace-pre-wrap">
|
{run.content}
|
||||||
{run.message.content}
|
</p>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-500 mt-2">
|
||||||
|
{formatDate(run.createdAt)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-500 mt-2">
|
|
||||||
{formatDate(run.message.created_at)}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : (
|
||||||
|
<div className="flex gap-3">
|
||||||
{/* Assistant Response */}
|
<div className="flex-shrink-0">
|
||||||
<div className="flex gap-3">
|
<div className="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
|
||||||
<div className="flex-shrink-0">
|
<Bot className="w-4 h-4 text-green-600" />
|
||||||
<div className="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
|
</div>
|
||||||
<Bot className="w-4 h-4 text-green-600" />
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="bg-gray-50 border rounded-lg p-4 shadow-sm">
|
||||||
|
<div
|
||||||
|
className="text-gray-900 whitespace-pre-wrap prose prose-sm max-w-none"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: run.content.replace(/\n/g, "<br>"),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-500 mt-2">
|
||||||
|
{formatDate(run.createdAt)}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
)}
|
||||||
<div className="bg-gray-50 border rounded-lg p-4 shadow-sm">
|
|
||||||
<div
|
|
||||||
className="text-gray-900 whitespace-pre-wrap prose prose-sm max-w-none"
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: run.response.content.replace(/\n/g, "<br>"),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-gray-500 mt-2">
|
|
||||||
{formatDate(run.response.created_at)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ export default function HistoryPage() {
|
||||||
if (uid) setUserId(uid);
|
if (uid) setUserId(uid);
|
||||||
try {
|
try {
|
||||||
const result = await getChatHistory(uid ?? "user_123");
|
const result = await getChatHistory(uid ?? "user_123");
|
||||||
setSessions(result.sessions);
|
setSessions(result.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching sessions:", error);
|
console.error("Error fetching sessions:", error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -81,7 +81,7 @@ export default function HistoryPage() {
|
||||||
const filteredSessions = sessions.filter(
|
const filteredSessions = sessions.filter(
|
||||||
(session) =>
|
(session) =>
|
||||||
session.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
session.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
session.session_id.toLowerCase().includes(searchTerm.toLowerCase())
|
session.sessionId.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -170,7 +170,7 @@ export default function HistoryPage() {
|
||||||
<div className="flex items-center gap-4 text-sm text-gray-500">
|
<div className="flex items-center gap-4 text-sm text-gray-500">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Clock className="w-4 h-4" />
|
<Clock className="w-4 h-4" />
|
||||||
<span>{formatDate(session.created_at)}</span>
|
<span>{formatDate(session.createdAt)}</span>
|
||||||
</div>
|
</div>
|
||||||
{/* <div className="flex items-center gap-1">
|
{/* <div className="flex items-center gap-1">
|
||||||
<MessageSquare className="w-4 h-4" />
|
<MessageSquare className="w-4 h-4" />
|
||||||
|
|
@ -187,8 +187,8 @@ export default function HistoryPage() {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 text-xs text-gray-400">
|
<div className="mt-2 text-xs text-gray-400">
|
||||||
<p>Agent ID: {session.agent_id}</p>
|
<p>Agent ID: {session.agentId}</p>
|
||||||
<p>Updated: {formatDate(session.updated_at)}</p>
|
<p>Updated: {formatDate(session.updatedAt)}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right text-sm text-gray-500">
|
<div className="text-right text-sm text-gray-500">
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import Sidebar from "@/components/sidebar";
|
||||||
|
import EbookForm from "@/components/form/ebook-form";
|
||||||
|
|
||||||
|
export default function CreateEbookPage() {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen">
|
||||||
|
<Sidebar />
|
||||||
|
<main className="flex-1 bg-gray-50 p-6">
|
||||||
|
<EbookForm mode="create" />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,20 @@
|
||||||
import ManajemenEbookDetail from "@/components/form/manajemen-ebook-detail";
|
|
||||||
import Sidebar from "@/components/sidebar";
|
import Sidebar from "@/components/sidebar";
|
||||||
import ManajemenEbook from "@/components/table/manajemen-ebook-table";
|
import EbookDetail from "@/components/form/ebook-detail";
|
||||||
|
|
||||||
export default function ManajemenEbookDetailPage() {
|
interface PageProps {
|
||||||
|
params: {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ManajemenEbookDetailPage({ params }: PageProps) {
|
||||||
|
const ebookId = parseInt(params.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen">
|
<div className="flex min-h-screen">
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
<main className="flex-1 bg-white p-6">
|
<main className="flex-1 bg-gray-50 p-6">
|
||||||
<ManajemenEbookDetail />
|
<EbookDetail ebookId={ebookId} />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import Sidebar from "@/components/sidebar";
|
||||||
|
import EbookForm from "@/components/form/ebook-form";
|
||||||
|
|
||||||
|
interface PageProps {
|
||||||
|
params: {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EditEbookPage({ params }: PageProps) {
|
||||||
|
const ebookId = parseInt(params.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen">
|
||||||
|
<Sidebar />
|
||||||
|
<main className="flex-1 bg-gray-50 p-6">
|
||||||
|
<EbookForm ebookId={ebookId} mode="edit" />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -5,7 +5,7 @@ export default function ManajemenEbookPage() {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen">
|
<div className="flex min-h-screen">
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
<main className="flex-1 bg-white p-6">
|
<main className="flex-1 bg-gray-50 p-6">
|
||||||
<ManajemenEbook />
|
<ManajemenEbook />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,12 @@
|
||||||
import FeedBack from "@/components/dashboard/feedback";
|
|
||||||
import UserMemory from "@/components/dashboard/memory";
|
|
||||||
import Menu from "@/components/dashboard/menu";
|
|
||||||
import FormManajemenUser from "@/components/form/manajement-user";
|
|
||||||
import Sidebar from "@/components/sidebar";
|
import Sidebar from "@/components/sidebar";
|
||||||
import ManajemenUser from "@/components/table/manajemen-user-table";
|
import UserForm from "@/components/form/user-form";
|
||||||
|
|
||||||
export default function CreateManajemenUserPage() {
|
export default function CreateUserPage() {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen">
|
<div className="flex min-h-screen">
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
<main className="flex-1 bg-white p-6">
|
<main className="flex-1 bg-gray-50 p-6">
|
||||||
<FormManajemenUser />
|
<UserForm mode="create" />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import Sidebar from "@/components/sidebar";
|
||||||
|
import UserDetail from "@/components/form/user-detail";
|
||||||
|
|
||||||
|
interface PageProps {
|
||||||
|
params: {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UserDetailPage({ params }: PageProps) {
|
||||||
|
const userId = parseInt(params.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen">
|
||||||
|
<Sidebar />
|
||||||
|
<main className="flex-1 bg-gray-50 p-6">
|
||||||
|
<UserDetail userId={userId} />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import Sidebar from "@/components/sidebar";
|
||||||
|
import UserForm from "@/components/form/user-form";
|
||||||
|
|
||||||
|
interface PageProps {
|
||||||
|
params: {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EditUserPage({ params }: PageProps) {
|
||||||
|
const userId = parseInt(params.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen">
|
||||||
|
<Sidebar />
|
||||||
|
<main className="flex-1 bg-gray-50 p-6">
|
||||||
|
<UserForm userId={userId} mode="edit" />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,3 @@
|
||||||
import FeedBack from "@/components/dashboard/feedback";
|
|
||||||
import UserMemory from "@/components/dashboard/memory";
|
|
||||||
import Menu from "@/components/dashboard/menu";
|
|
||||||
import Sidebar from "@/components/sidebar";
|
import Sidebar from "@/components/sidebar";
|
||||||
import ManajemenUser from "@/components/table/manajemen-user-table";
|
import ManajemenUser from "@/components/table/manajemen-user-table";
|
||||||
|
|
||||||
|
|
@ -8,7 +5,7 @@ export default function ManajemenUserPage() {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen">
|
<div className="flex min-h-screen">
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
<main className="flex-1 bg-white p-6">
|
<main className="flex-1 bg-gray-50 p-6">
|
||||||
<ManajemenUser />
|
<ManajemenUser />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,20 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
import LiveConsultation from "@/components/dashboard/live-consultation";
|
import LiveConsultation from "@/components/dashboard/live-consultation";
|
||||||
import Profile from "@/components/dashboard/profile";
|
import Profile from "@/components/dashboard/profile";
|
||||||
import Sidebar from "@/components/sidebar";
|
import Sidebar from "@/components/sidebar";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export default function ProfilePage() {
|
export default function ProfilePage() {
|
||||||
|
const [hasMounted, setHasMounted] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setHasMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!hasMounted) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen">
|
<div className="flex min-h-screen">
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
"use client";
|
||||||
|
import ScheduleForm from "@/components/form/schedule-form";
|
||||||
|
import Sidebar from "@/components/sidebar";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export default function CreateSchedulePage() {
|
||||||
|
const [hasMounted, setHasMounted] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setHasMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!hasMounted) {
|
||||||
|
return <>{null}</>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen">
|
||||||
|
<Sidebar />
|
||||||
|
<main className="flex-1 bg-white p-6">
|
||||||
|
<ScheduleForm />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,21 @@
|
||||||
// app/chat/page.tsx
|
// app/chat/page.tsx
|
||||||
|
"use client";
|
||||||
|
|
||||||
import Agents from "@/components/dashboard/agents";
|
|
||||||
import Chat from "@/components/dashboard/chat";
|
|
||||||
import Schedule from "@/components/dashboard/schedule";
|
import Schedule from "@/components/dashboard/schedule";
|
||||||
import Sidebar from "@/components/sidebar";
|
import Sidebar from "@/components/sidebar";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
export default function SchedulePage() {
|
export default function SchedulePage() {
|
||||||
|
const [hasMounted, setHasMounted] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setHasMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!hasMounted) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen">
|
<div className="flex min-h-screen">
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import CreateUserForm from "@/components/form/users/create-user-form";
|
||||||
|
import Sidebar from "@/components/sidebar";
|
||||||
|
|
||||||
|
export default function UsersManagementCreate() {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen">
|
||||||
|
<Sidebar />
|
||||||
|
<main className="flex-1 bg-white p-6">
|
||||||
|
<CreateUserForm />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import DetailUserForm from "@/components/form/users/detail-user-form";
|
||||||
|
import Sidebar from "@/components/sidebar";
|
||||||
|
|
||||||
|
export default function UsersManagementCreate() {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen">
|
||||||
|
<Sidebar />
|
||||||
|
<main className="flex-1 bg-white p-6">
|
||||||
|
<DetailUserForm />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import Sidebar from "@/components/sidebar";
|
||||||
|
import UsersManagementTable from "@/components/table/expert-approver/users-management";
|
||||||
|
|
||||||
|
export default function UsersManagement() {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen">
|
||||||
|
<Sidebar />
|
||||||
|
<main className="flex-1 bg-white p-6">
|
||||||
|
<UsersManagementTable />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -2,9 +2,5 @@ import Login from "@/components/form/login";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export default function AuthPage() {
|
export default function AuthPage() {
|
||||||
return (
|
return <Login />;
|
||||||
<>
|
|
||||||
<Login />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,5 @@ import SignUp from "@/components/form/sign-up";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export default function AuthSignUpPage() {
|
export default function AuthSignUpPage() {
|
||||||
return (
|
return <SignUp />;
|
||||||
<>
|
|
||||||
<SignUp />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
import ClientOnly from "@/components/client-only";
|
||||||
|
import ToastProvider from "@/components/toast-provider";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-geist-sans",
|
||||||
|
|
@ -22,12 +24,17 @@ export default function RootLayout({
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body
|
<body
|
||||||
|
suppressHydrationWarning
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
<ClientOnly>
|
||||||
|
<ToastProvider />
|
||||||
|
</ClientOnly>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
interface ClientOnlyProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
fallback?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ClientOnly({ children, fallback = null }: ClientOnlyProps) {
|
||||||
|
const [hasMounted, setHasMounted] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setHasMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!hasMounted) {
|
||||||
|
return <>{fallback}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,158 @@
|
||||||
|
"use client";
|
||||||
|
import {
|
||||||
|
Pagination,
|
||||||
|
PaginationContent,
|
||||||
|
PaginationEllipsis,
|
||||||
|
PaginationItem,
|
||||||
|
PaginationLink,
|
||||||
|
PaginationNext,
|
||||||
|
PaginationPrevious,
|
||||||
|
} from "@/components/ui/pagination";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export default function CustomPagination(props: {
|
||||||
|
totalPage: number;
|
||||||
|
onPageChange: (data: number) => void;
|
||||||
|
size?: string;
|
||||||
|
}) {
|
||||||
|
const { totalPage, onPageChange } = props;
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onPageChange(page);
|
||||||
|
}, [page]);
|
||||||
|
|
||||||
|
const renderPageNumbers = () => {
|
||||||
|
const pageNumbers = [];
|
||||||
|
const halfWindow = Math.floor(5 / 2);
|
||||||
|
|
||||||
|
let startPage = page - halfWindow;
|
||||||
|
let endPage = page + halfWindow;
|
||||||
|
|
||||||
|
if (startPage < 2) {
|
||||||
|
endPage += 2 - startPage;
|
||||||
|
startPage = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endPage > totalPage - 1) {
|
||||||
|
startPage -= endPage - (totalPage - 1);
|
||||||
|
endPage = totalPage - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
startPage = Math.max(2, startPage);
|
||||||
|
endPage = Math.min(totalPage - 1, endPage);
|
||||||
|
|
||||||
|
for (let i = startPage; i <= endPage; i++) {
|
||||||
|
pageNumbers.push(
|
||||||
|
<PaginationItem key={i} onClick={() => setPage(i)}>
|
||||||
|
<PaginationLink isActive={page === i}>{i}</PaginationLink>
|
||||||
|
</PaginationItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pageNumbers;
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Pagination className="mx-0 !justify-end w-fit">
|
||||||
|
<PaginationContent>
|
||||||
|
{page - 10 > 0 && (
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationLink
|
||||||
|
onClick={() => (page > 10 ? setPage(page - 10) : setPage(1))}
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
{/* <DoubleArrowLeftIcon /> */}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M18.41 7.41L17 6l-6 6l6 6l1.41-1.41L13.83 12zm-6 0L11 6l-6 6l6 6l1.41-1.41L7.83 12z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</PaginationLink>
|
||||||
|
</PaginationItem>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<PaginationItem className="hidden md:block">
|
||||||
|
<PaginationPrevious
|
||||||
|
onClick={() => (page > 1 ? setPage(page - 1) : "")}
|
||||||
|
className="cursor-pointer"
|
||||||
|
/>
|
||||||
|
</PaginationItem>
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationLink
|
||||||
|
onClick={() => setPage(1)}
|
||||||
|
isActive={page === 1}
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
{1}
|
||||||
|
</PaginationLink>
|
||||||
|
</PaginationItem>
|
||||||
|
{page > 5 && (
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationEllipsis
|
||||||
|
onClick={() => setPage(page - 1)}
|
||||||
|
className="cursor-pointer"
|
||||||
|
/>
|
||||||
|
</PaginationItem>
|
||||||
|
)}
|
||||||
|
{renderPageNumbers()}
|
||||||
|
{page < totalPage - 4 && (
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationEllipsis
|
||||||
|
onClick={() => setPage(page + 1)}
|
||||||
|
className="cursor-pointer"
|
||||||
|
/>
|
||||||
|
</PaginationItem>
|
||||||
|
)}
|
||||||
|
{totalPage > 1 && (
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationLink
|
||||||
|
onClick={() => setPage(totalPage)}
|
||||||
|
isActive={page === totalPage}
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
{totalPage}
|
||||||
|
</PaginationLink>
|
||||||
|
</PaginationItem>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<PaginationItem className="hidden md:block">
|
||||||
|
<PaginationNext
|
||||||
|
onClick={() => (page < totalPage ? setPage(page + 1) : "")}
|
||||||
|
className="cursor-pointer"
|
||||||
|
/>
|
||||||
|
</PaginationItem>
|
||||||
|
|
||||||
|
{page + 10 < totalPage && totalPage > 9 && (
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationLink
|
||||||
|
onClick={() =>
|
||||||
|
page < totalPage - 10 ? setPage(page + 10) : setPage(totalPage)
|
||||||
|
}
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
{/* <DoubleArrowRightIcon /> */}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M5.59 7.41L7 6l6 6l-6 6l-1.41-1.41L10.17 12zm6 0L13 6l6 6l-6 6l-1.41-1.41L16.17 12z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</PaginationLink>
|
||||||
|
</PaginationItem>
|
||||||
|
)}
|
||||||
|
</PaginationContent>
|
||||||
|
</Pagination>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -42,9 +42,11 @@ export default function Chat() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const agent = Cookies.get("selected_expert_id");
|
const agent = Cookies.get("selected_expert_id");
|
||||||
const uid = Cookies.get("uid");
|
const uid = Cookies.get("uid");
|
||||||
|
const userId = Cookies.get("uie");
|
||||||
|
|
||||||
if (agent) setAgentId(agent);
|
if (agent) setAgentId(agent);
|
||||||
if (uid) setUserId(uid);
|
if (userId) setUserId(userId);
|
||||||
|
// if (uid) setUserId(uid);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -128,9 +130,11 @@ export default function Chat() {
|
||||||
const agent = Cookies.get("selected_expert_id");
|
const agent = Cookies.get("selected_expert_id");
|
||||||
const uid = Cookies.get("uid");
|
const uid = Cookies.get("uid");
|
||||||
const sessId = Cookies.get("session_id");
|
const sessId = Cookies.get("session_id");
|
||||||
|
const userId = Cookies.get("uie");
|
||||||
|
|
||||||
if (agent) setAgentId(agent);
|
if (agent) setAgentId(agent);
|
||||||
if (uid) setUserId(uid);
|
if (userId) setUserId(userId);
|
||||||
|
// if (uid) setUserId(uid);
|
||||||
if (sessId) setSessionId(sessId);
|
if (sessId) setSessionId(sessId);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
@ -194,7 +198,7 @@ export default function Chat() {
|
||||||
try {
|
try {
|
||||||
const updatedMessages = [...messages, userMessage, assistantMessage];
|
const updatedMessages = [...messages, userMessage, assistantMessage];
|
||||||
await saveChatHistory({
|
await saveChatHistory({
|
||||||
user_id: userId,
|
user_id: parseInt(userId),
|
||||||
agent_id: agentId,
|
agent_id: agentId,
|
||||||
session_id: currentSessionId,
|
session_id: currentSessionId,
|
||||||
title: currentInput.substring(0, 100),
|
title: currentInput.substring(0, 100),
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,118 @@
|
||||||
"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 { getUserDetail, updateUser, UserData } from "@/service/user";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import { Input } from "../ui/input";
|
||||||
|
import { getProfile } from "@/service/master-user";
|
||||||
|
import Cookies from "js-cookie";
|
||||||
|
|
||||||
export default function Profile() {
|
export default function Profile() {
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
const token = Cookies.get("access_token");
|
||||||
|
const userId = Number(Cookies.get("uie"));
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [editMode, setEditMode] = useState(false);
|
||||||
|
const [userType, setUserType] = useState<"tenaga_ahli" | "pengguna_umum">(
|
||||||
|
"tenaga_ahli"
|
||||||
|
);
|
||||||
|
const [formData, setFormData] = useState<UserData>({
|
||||||
|
username: "",
|
||||||
|
email: "",
|
||||||
|
fullname: "",
|
||||||
|
address: "",
|
||||||
|
phoneNumber: "",
|
||||||
|
whatsappNumber: "",
|
||||||
|
password: "",
|
||||||
|
dateOfBirth: "",
|
||||||
|
genderType: "male",
|
||||||
|
degree: "",
|
||||||
|
userLevelId: 1,
|
||||||
|
userRoleId: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadUserData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadUserData = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await getProfile(token);
|
||||||
|
if (!response.error && response.data?.data) {
|
||||||
|
const data = response?.data?.data;
|
||||||
|
setFormData({
|
||||||
|
username: data.username || "",
|
||||||
|
email: data.email || "",
|
||||||
|
fullname: data.fullname || "",
|
||||||
|
address: data.address || "",
|
||||||
|
phoneNumber: data.phoneNumber || "",
|
||||||
|
whatsappNumber: data.whatsappNumber || "",
|
||||||
|
password: "",
|
||||||
|
dateOfBirth: data.dateOfBirth || "",
|
||||||
|
genderType: (data.genderType as "male" | "female") || "male",
|
||||||
|
degree: data.degree || "",
|
||||||
|
userLevelId: data.userLevelId || 1,
|
||||||
|
userRoleId: data.userRoleId || 3,
|
||||||
|
});
|
||||||
|
console.log("profile", response);
|
||||||
|
setUserType(data.userRoleId === 2 ? "tenaga_ahli" : "pengguna_umum");
|
||||||
|
} else {
|
||||||
|
toast.error(response.message || "Gagal memuat data user");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Gagal memuat data user");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setFormData({ ...formData, [e.target.name]: e.target.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!userId) return toast.error("User ID tidak ditemukan");
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const updateDataWithoutPassword = {
|
||||||
|
address: formData.address,
|
||||||
|
dateOfBirth: formData.dateOfBirth,
|
||||||
|
degree: formData.degree,
|
||||||
|
email: formData.email,
|
||||||
|
fullname: formData.fullname,
|
||||||
|
phoneNumber: formData.phoneNumber,
|
||||||
|
statusId: 1,
|
||||||
|
userLevelId: formData.userLevelId,
|
||||||
|
userRoleId: formData.userRoleId,
|
||||||
|
username: formData.username,
|
||||||
|
whatsappNumber: formData.whatsappNumber,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await updateUser(userId, updateDataWithoutPassword);
|
||||||
|
|
||||||
|
if (response?.message === 200) {
|
||||||
|
toast.success("Profile berhasil diupdate!");
|
||||||
|
setEditMode(false);
|
||||||
|
} else {
|
||||||
|
toast.error(
|
||||||
|
typeof response?.message === "string"
|
||||||
|
? response.message
|
||||||
|
: "Terjadi kesalahan"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Gagal update profile");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-7xl mx-auto">
|
<div className="max-w-7xl mx-auto">
|
||||||
|
|
@ -37,9 +143,30 @@ export default function Profile() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button className="bg-blue-600 text-white px-4 py-2 rounded-md text-sm">
|
{!editMode ? (
|
||||||
EDIT PROFILE
|
<button
|
||||||
</button>
|
className="bg-blue-600 text-white px-4 py-2 rounded-md text-sm"
|
||||||
|
onClick={() => setEditMode(true)}
|
||||||
|
>
|
||||||
|
EDIT PROFILE
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
className="bg-gray-400 text-white px-4 py-2 rounded-md text-sm"
|
||||||
|
onClick={() => setEditMode(false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="bg-green-600 text-white px-4 py-2 rounded-md text-sm"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? "Saving..." : "Submit"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Profile Form */}
|
{/* Profile Form */}
|
||||||
|
|
@ -47,20 +174,53 @@ export default function Profile() {
|
||||||
<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
|
<Input
|
||||||
label="Nama Lengkap"
|
name="fullname"
|
||||||
value="Prof. Dr. Albertus Wahyurudhanto, M.Si"
|
placeholder="Masukkan Nama Lengkap"
|
||||||
|
value={formData.fullname}
|
||||||
|
onChange={handleChange}
|
||||||
|
disabled={!editMode}
|
||||||
/>
|
/>
|
||||||
<Input label="Gelar" value="Profesor, Doctor, M.Si" />
|
<Input
|
||||||
<Input label="Pendidikan Terakhir" value="S3" />
|
name="degree"
|
||||||
<Input label="Pekerjaan Terakhir" value="Konsultan" />
|
placeholder="Gelar"
|
||||||
<Input label="Email" value="albertus@example.com" />
|
value={formData.degree}
|
||||||
<Input label="No Whatsapp" value="085112341234" />
|
onChange={handleChange}
|
||||||
|
disabled={!editMode}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
name="userLevelId"
|
||||||
|
placeholder="Pendidikan Terakhir"
|
||||||
|
value={formData.userLevelId === 2 ? "S2/S3" : "S1"}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
name="address"
|
||||||
|
placeholder="Pekerjaan Terakhir"
|
||||||
|
value={formData.address}
|
||||||
|
onChange={handleChange}
|
||||||
|
disabled={!editMode}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
name="email"
|
||||||
|
placeholder="Email"
|
||||||
|
value={formData.email}
|
||||||
|
onChange={handleChange}
|
||||||
|
disabled={!editMode}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
name="whatsappNumber"
|
||||||
|
placeholder="No Whatsapp"
|
||||||
|
value={formData.whatsappNumber}
|
||||||
|
onChange={handleChange}
|
||||||
|
disabled={!editMode}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="col-span-2">
|
<div className="col-span-2">
|
||||||
<label className="text-sm text-gray-600">Kata Sandi</label>
|
<label className="text-sm text-gray-600">Kata Sandi</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
type={showPassword ? "text" : "password"}
|
type={showPassword ? "text" : "password"}
|
||||||
value="password123"
|
value="********"
|
||||||
disabled
|
disabled
|
||||||
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"
|
||||||
/>
|
/>
|
||||||
|
|
@ -98,10 +258,13 @@ export default function Profile() {
|
||||||
},
|
},
|
||||||
].map((item, i) => (
|
].map((item, i) => (
|
||||||
<div key={i} className="grid grid-cols-5 gap-4 items-end">
|
<div key={i} className="grid grid-cols-5 gap-4 items-end">
|
||||||
<Input label="Universitas" value={item.univ} />
|
<Input placeholder="Universitas" defaultValue={item.univ} />
|
||||||
<Input label="Jurusan" value={item.jurusan} />
|
<Input placeholder="Jurusan" defaultValue={item.jurusan} />
|
||||||
<Input label="Tingkat Pendidikan" value={item.tingkat} />
|
<Input
|
||||||
<Input label="Tahun Lulus" value={item.tahun} />
|
placeholder="Tingkat Pendidikan"
|
||||||
|
defaultValue={item.tingkat}
|
||||||
|
/>
|
||||||
|
<Input placeholder="Tahun Lulus" defaultValue={item.tahun} />
|
||||||
<button className="bg-blue-600 text-white rounded-md px-3 py-2 text-sm flex items-center gap-2">
|
<button 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>
|
</button>
|
||||||
|
|
@ -115,12 +278,12 @@ export default function Profile() {
|
||||||
<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">
|
<div className="grid grid-cols-4 gap-4">
|
||||||
<Input
|
<Input
|
||||||
label="Title Pekerjaan"
|
placeholder="Title Pekerjaan"
|
||||||
value="Konsultan Komunikasi Pemerintahan"
|
defaultValue="Konsultan Komunikasi Pemerintahan"
|
||||||
/>
|
/>
|
||||||
<Input label="Nama Perusahaan" value="Perusahaan ABC" />
|
<Input placeholder="Nama Perusahaan" defaultValue="Perusahaan ABC" />
|
||||||
<Input label="Tanggal Mulai" value="2000" />
|
<Input placeholder="Tanggal Mulai" defaultValue="2000" />
|
||||||
<Input label="Tanggal Selesai" value="Sekarang" />
|
<Input placeholder="Tanggal Selesai" defaultValue="Sekarang" />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
@ -162,16 +325,16 @@ export default function Profile() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reusable Input Component
|
// Reusable Input Component
|
||||||
function Input({ label, value }: { label: string; value: string }) {
|
// function Input({ label, value }: { label: string; value: string }) {
|
||||||
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
|
// disabled
|
||||||
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>
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
|
||||||
|
|
@ -5,74 +5,123 @@ import { Button } from "../ui/button";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import { Input } from "../ui/input";
|
import { Input } from "../ui/input";
|
||||||
import { BellIcon } from "lucide-react";
|
import { BellIcon } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { getSchedulesData } from "@/service/schedule";
|
||||||
|
import { convertDateFormat, convertDateFormatNoTime } from "@/utils/globals";
|
||||||
|
|
||||||
|
const roleId = Cookies.get("urie");
|
||||||
|
const timeList = [
|
||||||
|
{ id: 1, time: "07:00" },
|
||||||
|
{ id: 2, time: "08:00" },
|
||||||
|
{ id: 3, time: "09:00" },
|
||||||
|
{ id: 4, time: "10:00" },
|
||||||
|
{ id: 5, time: "11:00" },
|
||||||
|
{ id: 6, time: "12:00" },
|
||||||
|
{ id: 7, time: "13:00" },
|
||||||
|
{ id: 8, time: "14:00" },
|
||||||
|
{ id: 9, time: "15:00" },
|
||||||
|
{ id: 10, time: "16:00" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const getWeekDates = (start: Date) => {
|
||||||
|
const dates: string[] = [];
|
||||||
|
for (let i = 0; i < 7; i++) {
|
||||||
|
const d = new Date(start);
|
||||||
|
d.setDate(start.getDate() + i);
|
||||||
|
dates.push(d.toISOString().slice(0, 10));
|
||||||
|
}
|
||||||
|
return dates;
|
||||||
|
};
|
||||||
|
|
||||||
|
const today = new Date();
|
||||||
|
|
||||||
|
const test = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: "Chat dengan Prof. Dr. Ali",
|
||||||
|
date: "2025-08-25",
|
||||||
|
time: "09:00",
|
||||||
|
color: "bg-blue-200 text-blue-800",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: "Chat dengan Dr. Ramzi Kurniawan",
|
||||||
|
date: "2025-08-27",
|
||||||
|
time: "08:00",
|
||||||
|
color: "bg-blue-300 text-blue-900",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: "Re-schedule Dr. Ramzi",
|
||||||
|
date: "2025-08-27",
|
||||||
|
time: "12:00",
|
||||||
|
color: "bg-orange-200 text-orange-900",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
interface ChatSession {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ScheduleType {
|
||||||
|
id: number;
|
||||||
|
chat_session_id: number;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
summary: string;
|
||||||
|
duration: number;
|
||||||
|
scheduled_at: Date;
|
||||||
|
chat_session: ChatSession;
|
||||||
|
}
|
||||||
export default function Schedule() {
|
export default function Schedule() {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const roleId = Cookies.get("urie");
|
const [startDate, setStartDate] = useState(today);
|
||||||
const timeList = [
|
const dateAWeek = getWeekDates(startDate);
|
||||||
{ id: 1, time: "07:00" },
|
const [events, setEvents] = useState<ScheduleType[]>([]);
|
||||||
{ id: 2, time: "08:00" },
|
|
||||||
{ id: 3, time: "09:00" },
|
|
||||||
{ id: 4, time: "10:00" },
|
|
||||||
{ id: 5, time: "11:00" },
|
|
||||||
{ id: 6, time: "12:00" },
|
|
||||||
{ id: 7, time: "13:00" },
|
|
||||||
{ id: 8, time: "14:00" },
|
|
||||||
{ id: 9, time: "15:00" },
|
|
||||||
{ id: 10, time: "16:00" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const getWeekDates = (start: Date) => {
|
useEffect(() => {
|
||||||
const dates: string[] = [];
|
getSchedules();
|
||||||
for (let i = 0; i < 7; i++) {
|
}, []);
|
||||||
const d = new Date(start);
|
|
||||||
d.setDate(start.getDate() + i);
|
const getSchedules = async () => {
|
||||||
dates.push(d.toISOString().slice(0, 10));
|
const res = await getSchedulesData({});
|
||||||
}
|
setEvents(res?.data);
|
||||||
return dates;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const today = new Date("2025-08-25");
|
|
||||||
const [startDate, setStartDate] = useState(today);
|
|
||||||
|
|
||||||
const dateAWeek = getWeekDates(startDate);
|
|
||||||
|
|
||||||
const [events] = useState([
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
title: "Chat dengan Prof. Dr. Ali",
|
|
||||||
date: "2025-08-25",
|
|
||||||
time: "09:00",
|
|
||||||
color: "bg-blue-200 text-blue-800",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
title: "Chat dengan Dr. Ramzi Kurniawan",
|
|
||||||
date: "2025-08-27",
|
|
||||||
time: "08:00",
|
|
||||||
color: "bg-blue-300 text-blue-900",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
title: "Re-schedule Dr. Ramzi",
|
|
||||||
date: "2025-08-27",
|
|
||||||
time: "12:00",
|
|
||||||
color: "bg-orange-200 text-orange-900",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const setItemSchedule = (time: string, date: string) => {
|
const setItemSchedule = (time: string, date: string) => {
|
||||||
const found = events.find((ev) => ev.time === time && ev.date === date);
|
const now = new Date(); // waktu saat ini
|
||||||
|
|
||||||
|
// Gabungkan date + time jadi ISO string
|
||||||
|
const scheduled = new Date(`${date}T${time}:00+07:00`);
|
||||||
|
// contoh hasil: 2025-09-24T11:00:00+07:00
|
||||||
|
|
||||||
|
const found = events.find((ev) => {
|
||||||
|
const eventDate = new Date(ev.scheduled_at);
|
||||||
|
return (
|
||||||
|
eventDate.getFullYear() === scheduled.getFullYear() &&
|
||||||
|
eventDate.getMonth() === scheduled.getMonth() &&
|
||||||
|
eventDate.getDate() === scheduled.getDate() &&
|
||||||
|
eventDate.getHours() === scheduled.getHours()
|
||||||
|
);
|
||||||
|
});
|
||||||
if (found) {
|
if (found) {
|
||||||
|
const isPast = scheduled < now; // cek apakah sudah lewat dari waktu sekarang
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`rounded p-1 text-xs font-medium text-center ${found.color}`}
|
className={`py-4 text-xs font-medium text-center ${
|
||||||
|
isPast
|
||||||
|
? "bg-gray-300 text-gray-900" // warna kalau event sudah lewat
|
||||||
|
: "bg-blue-300 text-blue-900" // warna default jika masih akan datang
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{found.title}
|
{found.title}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return "";
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePrev = () => {
|
const handlePrev = () => {
|
||||||
|
|
@ -107,10 +156,12 @@ export default function Schedule() {
|
||||||
<div className="flex w-full h-full">
|
<div className="flex w-full h-full">
|
||||||
<div className="flex-1 bg-white rounded-lg ">
|
<div className="flex-1 bg-white rounded-lg ">
|
||||||
<div className="flex flex-col justify-between items-start mb-4 gap-2">
|
<div className="flex flex-col justify-between items-start mb-4 gap-2">
|
||||||
{roleId === "4" && (
|
{roleId === "3" && (
|
||||||
<button className="px-4 py-2 bg-blue-500 text-white rounded-lg">
|
<Link href="/admin/schedule/create">
|
||||||
Add New
|
<button className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600">
|
||||||
</button>
|
Add New
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
)}
|
)}
|
||||||
<div className="space-x-0.5 flex items-center">
|
<div className="space-x-0.5 flex items-center">
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -130,7 +181,12 @@ export default function Schedule() {
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</Button>
|
</Button>
|
||||||
<p className="bg-gray-400 p-1 text-md">Today</p>
|
<a
|
||||||
|
className="bg-gray-400 p-1 text-md cursor-pointer"
|
||||||
|
onClick={() => setStartDate(today)}
|
||||||
|
>
|
||||||
|
Today
|
||||||
|
</a>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleNext}
|
onClick={handleNext}
|
||||||
size={"sm"}
|
size={"sm"}
|
||||||
|
|
@ -196,7 +252,7 @@ export default function Schedule() {
|
||||||
<tr key={t.id}>
|
<tr key={t.id}>
|
||||||
<td className="text-center border py-4">{t.time}</td>
|
<td className="text-center border py-4">{t.time}</td>
|
||||||
{dateAWeek.map((d) => (
|
{dateAWeek.map((d) => (
|
||||||
<td key={d} className="border py-2 text-center">
|
<td key={d} className="border text-center">
|
||||||
{setItemSchedule(t.time, d)}
|
{setItemSchedule(t.time, d)}
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
|
|
@ -210,27 +266,47 @@ export default function Schedule() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-80 bg-[#EFF6FF99] border-l p-4">
|
<div className="w-80 bg-[#EFF6FF99] border-l p-4">
|
||||||
<h2 className="font-semibold text-lg mb-2">Agustus 2025</h2>
|
<h2 className="font-semibold text-lg mb-2">
|
||||||
<p className="text-sm text-gray-500 mb-4">TODAY 24-08-2025</p>
|
{new Intl.DateTimeFormat("id-ID", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
}).format(today)}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-gray-500 mb-4">
|
||||||
|
TODAY {convertDateFormatNoTime(today)}
|
||||||
|
</p>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="border rounded-lg p-2 text-sm">
|
{events.map((list, index) =>
|
||||||
<p className="font-medium">
|
new Date(list.scheduled_at).setHours(0, 0, 0, 0) >
|
||||||
Chat dengan Dr. Ramzi Kurniawan, S.Pd., M.P.
|
today.setHours(0, 0, 0, 0) ? (
|
||||||
</p>
|
<div
|
||||||
<p className="text-gray-500">08:00 - 09:00 AM</p>
|
key={String(list.id) + list.title + String(index)}
|
||||||
</div>
|
className="border rounded-lg p-2 text-sm"
|
||||||
<div className="border rounded-lg p-2 text-sm bg-orange-100">
|
>
|
||||||
|
<p className="font-medium">
|
||||||
|
Chat Session : {list.chat_session.name}
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-500">
|
||||||
|
{convertDateFormatNoTime(list.scheduled_at)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* <div className="border rounded-lg p-2 text-sm bg-orange-100">
|
||||||
<p className="font-medium">
|
<p className="font-medium">
|
||||||
Dr. Ramzi Kurniawan, S.Pd., M.P. re-schedule
|
Dr. Ramzi Kurniawan, S.Pd., M.P. re-schedule
|
||||||
</p>
|
</p>
|
||||||
<p className="text-gray-500">12:00 - 1:00 PM</p>
|
<p className="text-gray-500">12:00 - 1:00 PM</p>
|
||||||
</div>
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
|
{/*
|
||||||
<a href="#" className="text-blue-500 text-sm mt-4 block">
|
<a href="#" className="text-blue-500 text-sm mt-4 block">
|
||||||
Lihat Selengkapnya
|
Lihat Selengkapnya
|
||||||
</a>
|
</a> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,312 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { getEbookDetail, EbookDetailResponse } from "@/service/ebook";
|
||||||
|
import { toast } from "react-hot-toast";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Edit, Download, Eye, Calendar, User, BookOpen, DollarSign, Globe, Tag, FileText } from "lucide-react";
|
||||||
|
|
||||||
|
interface EbookDetailProps {
|
||||||
|
ebookId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EbookDetail({ ebookId }: EbookDetailProps) {
|
||||||
|
const router = useRouter();
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [ebookData, setEbookData] = useState<EbookDetailResponse["data"] | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadEbookData();
|
||||||
|
}, [ebookId]);
|
||||||
|
|
||||||
|
const loadEbookData = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await getEbookDetail(ebookId);
|
||||||
|
if (response.success) {
|
||||||
|
setEbookData(response.data);
|
||||||
|
} else {
|
||||||
|
toast.error("Gagal memuat data ebook");
|
||||||
|
router.push("/admin/management-ebook");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Terjadi kesalahan saat memuat data ebook");
|
||||||
|
router.push("/admin/management-ebook");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatCurrency = (amount: number) => {
|
||||||
|
return new Intl.NumberFormat('id-ID', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'IDR',
|
||||||
|
minimumFractionDigits: 0,
|
||||||
|
}).format(amount);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateString: string) => {
|
||||||
|
return new Date(dateString).toLocaleDateString('id-ID', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center h-64">
|
||||||
|
<div className="text-lg">Memuat data ebook...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ebookData) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center h-64">
|
||||||
|
<div className="text-lg text-red-500">Data ebook tidak ditemukan</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-6xl mx-auto space-y-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-semibold">{ebookData.title}</h1>
|
||||||
|
<p className="text-gray-600">Detail informasi ebook</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<Link href={`/admin/management-ebook/edit/${ebookId}`}>
|
||||||
|
<Button variant="outline" className="flex items-center space-x-2">
|
||||||
|
<Edit className="w-4 h-4" />
|
||||||
|
<span>Edit</span>
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<Button variant="outline" onClick={() => router.back()}>
|
||||||
|
Kembali
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
|
{/* Main Content */}
|
||||||
|
<div className="lg:col-span-2 space-y-6">
|
||||||
|
{/* Basic Information */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center space-x-2">
|
||||||
|
<BookOpen className="w-5 h-5" />
|
||||||
|
<span>Informasi Dasar</span>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">Judul</label>
|
||||||
|
<p className="text-lg font-semibold">{ebookData.title}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">Slug</label>
|
||||||
|
<p className="text-sm font-mono bg-gray-100 p-2 rounded">{ebookData.slug}</p>
|
||||||
|
</div>
|
||||||
|
<div className="md:col-span-2">
|
||||||
|
<label className="text-sm font-medium text-gray-500">Deskripsi</label>
|
||||||
|
<p className="text-sm mt-1">{ebookData.description}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">Kategori</label>
|
||||||
|
<Badge variant="secondary" className="mt-1">{ebookData.category}</Badge>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">Tags</label>
|
||||||
|
<p className="text-sm mt-1">{ebookData.tags}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Publication Details */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center space-x-2">
|
||||||
|
<FileText className="w-5 h-5" />
|
||||||
|
<span>Detail Publikasi</span>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">Penerbit</label>
|
||||||
|
<p className="text-sm mt-1">{ebookData.publisher}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">Tahun Terbit</label>
|
||||||
|
<p className="text-sm mt-1">{ebookData.publishedYear}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">Jumlah Halaman</label>
|
||||||
|
<p className="text-sm mt-1">{ebookData.pageCount} halaman</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">Bahasa</label>
|
||||||
|
<p className="text-sm mt-1">{ebookData.language}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">ISBN</label>
|
||||||
|
<p className="text-sm mt-1">{ebookData.isbn}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">Status Publikasi</label>
|
||||||
|
<Badge
|
||||||
|
variant={ebookData.isPublished ? "default" : "secondary"}
|
||||||
|
className="mt-1"
|
||||||
|
>
|
||||||
|
{ebookData.isPublished ? "Dipublikasikan" : "Draft"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Author Information */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center space-x-2">
|
||||||
|
<User className="w-5 h-5" />
|
||||||
|
<span>Informasi Penulis</span>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">Nama Penulis</label>
|
||||||
|
<p className="text-sm mt-1">{ebookData.authorName}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">Email Penulis</label>
|
||||||
|
<p className="text-sm mt-1">{ebookData.authorEmail}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sidebar */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Pricing */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center space-x-2">
|
||||||
|
<DollarSign className="w-5 h-5" />
|
||||||
|
<span>Harga</span>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-3xl font-bold text-green-600">
|
||||||
|
{formatCurrency(ebookData.price)}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500 mt-2">Harga per ebook</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Statistics */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Statistik</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-sm text-gray-500">Download</span>
|
||||||
|
<span className="font-semibold">{ebookData.downloadCount}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-sm text-gray-500">Pembelian</span>
|
||||||
|
<span className="font-semibold">{ebookData.purchaseCount}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-sm text-gray-500">Wishlist</span>
|
||||||
|
<span className="font-semibold">{ebookData.wishlistCount}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-sm text-gray-500">Rating</span>
|
||||||
|
<span className="font-semibold">{ebookData.rating}/5</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-sm text-gray-500">Review</span>
|
||||||
|
<span className="font-semibold">{ebookData.reviewCount}</span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* File Information */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>File</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{ebookData.pdfFileName ? (
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center space-x-2 mb-2">
|
||||||
|
<FileText className="w-4 h-4" />
|
||||||
|
<span className="text-sm font-medium">PDF File</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-600">{ebookData.pdfFileName}</p>
|
||||||
|
{ebookData.pdfFileSize && (
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
{(ebookData.pdfFileSize / 1024 / 1024).toFixed(2)} MB
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<Button size="sm" className="mt-2 w-full">
|
||||||
|
<Download className="w-4 h-4 mr-2" />
|
||||||
|
Download
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center text-gray-500">
|
||||||
|
<FileText className="w-8 h-8 mx-auto mb-2 opacity-50" />
|
||||||
|
<p className="text-sm">Tidak ada file PDF</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Timestamps */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center space-x-2">
|
||||||
|
<Calendar className="w-5 h-5" />
|
||||||
|
<span>Timeline</span>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">Dibuat</label>
|
||||||
|
<p className="text-sm mt-1">{formatDate(ebookData.createdAt)}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">Diperbarui</label>
|
||||||
|
<p className="text-sm mt-1">{formatDate(ebookData.updatedAt)}</p>
|
||||||
|
</div>
|
||||||
|
{ebookData.publishedAt && (
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">Dipublikasikan</label>
|
||||||
|
<p className="text-sm mt-1">{formatDate(ebookData.publishedAt)}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,328 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { createEbook, updateEbook, getEbookDetail, uploadEbookPdf, EbookData } from "@/service/ebook";
|
||||||
|
import { toast } from "react-hot-toast";
|
||||||
|
|
||||||
|
interface EbookFormProps {
|
||||||
|
ebookId?: number;
|
||||||
|
mode: "create" | "edit";
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EbookForm({ ebookId, mode }: EbookFormProps) {
|
||||||
|
const router = useRouter();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [uploadingFile, setUploadingFile] = useState(false);
|
||||||
|
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||||
|
const [formData, setFormData] = useState<EbookData>({
|
||||||
|
title: "",
|
||||||
|
slug: "",
|
||||||
|
description: "",
|
||||||
|
price: 0,
|
||||||
|
category: "",
|
||||||
|
tags: "",
|
||||||
|
pageCount: 0,
|
||||||
|
language: "Indonesia",
|
||||||
|
isbn: "",
|
||||||
|
publisher: "",
|
||||||
|
publishedYear: new Date().getFullYear(),
|
||||||
|
isPublished: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load ebook data for edit mode
|
||||||
|
useEffect(() => {
|
||||||
|
if (mode === "edit" && ebookId) {
|
||||||
|
loadEbookData();
|
||||||
|
}
|
||||||
|
}, [ebookId, mode]);
|
||||||
|
|
||||||
|
const loadEbookData = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await getEbookDetail(ebookId!);
|
||||||
|
if (response.success) {
|
||||||
|
const data = response.data;
|
||||||
|
setFormData({
|
||||||
|
title: data.title,
|
||||||
|
slug: data.slug,
|
||||||
|
description: data.description,
|
||||||
|
price: data.price,
|
||||||
|
category: data.category,
|
||||||
|
tags: data.tags,
|
||||||
|
pageCount: data.pageCount,
|
||||||
|
language: data.language,
|
||||||
|
isbn: data.isbn,
|
||||||
|
publisher: data.publisher,
|
||||||
|
publishedYear: data.publishedYear,
|
||||||
|
isPublished: data.isPublished,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Gagal memuat data ebook");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (field: keyof EbookData, value: any) => {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[field]: value
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
setSelectedFile(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
let response;
|
||||||
|
if (mode === "create") {
|
||||||
|
response = await createEbook(formData);
|
||||||
|
} else {
|
||||||
|
response = await updateEbook(ebookId!, formData);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response?.error) {
|
||||||
|
// Upload file if selected
|
||||||
|
if (selectedFile && response?.data?.data?.id) {
|
||||||
|
setUploadingFile(true);
|
||||||
|
const uploadResponse = await uploadEbookPdf(response.data.data.id, selectedFile);
|
||||||
|
if (uploadResponse?.error) {
|
||||||
|
toast.error("Ebook berhasil disimpan, namun gagal mengupload file PDF");
|
||||||
|
} else {
|
||||||
|
toast.success("Ebook dan file PDF berhasil disimpan");
|
||||||
|
}
|
||||||
|
setUploadingFile(false);
|
||||||
|
} else {
|
||||||
|
toast.success(`Ebook berhasil ${mode === "create" ? "dibuat" : "diperbarui"}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
router.push("/admin/management-ebook");
|
||||||
|
} else {
|
||||||
|
toast.error(response.message || "Terjadi kesalahan");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Terjadi kesalahan saat menyimpan ebook");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading && mode === "edit") {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center h-64">
|
||||||
|
<div className="text-lg">Memuat data...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-4xl mx-auto space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h1 className="text-2xl font-semibold">
|
||||||
|
{mode === "create" ? "Tambah Ebook Baru" : "Edit Ebook"}
|
||||||
|
</h1>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
>
|
||||||
|
Kembali
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
{/* Basic Information */}
|
||||||
|
<div className="bg-white p-6 rounded-lg border">
|
||||||
|
<h2 className="text-lg font-semibold mb-4">Informasi Dasar</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="title">Judul Ebook *</Label>
|
||||||
|
<Input
|
||||||
|
id="title"
|
||||||
|
value={formData.title}
|
||||||
|
onChange={(e) => handleInputChange("title", e.target.value)}
|
||||||
|
placeholder="Masukkan judul ebook"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="slug">Slug *</Label>
|
||||||
|
<Input
|
||||||
|
id="slug"
|
||||||
|
value={formData.slug}
|
||||||
|
onChange={(e) => handleInputChange("slug", e.target.value)}
|
||||||
|
placeholder="ebook-judul-contoh"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="md:col-span-2">
|
||||||
|
<Label htmlFor="description">Deskripsi *</Label>
|
||||||
|
<Textarea
|
||||||
|
id="description"
|
||||||
|
value={formData.description}
|
||||||
|
onChange={(e) => handleInputChange("description", e.target.value)}
|
||||||
|
placeholder="Masukkan deskripsi ebook"
|
||||||
|
rows={4}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="category">Kategori *</Label>
|
||||||
|
<Input
|
||||||
|
id="category"
|
||||||
|
value={formData.category}
|
||||||
|
onChange={(e) => handleInputChange("category", e.target.value)}
|
||||||
|
placeholder="Masukkan kategori"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="tags">Tags</Label>
|
||||||
|
<Input
|
||||||
|
id="tags"
|
||||||
|
value={formData.tags}
|
||||||
|
onChange={(e) => handleInputChange("tags", e.target.value)}
|
||||||
|
placeholder="tag1, tag2, tag3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Publication Details */}
|
||||||
|
<div className="bg-white p-6 rounded-lg border">
|
||||||
|
<h2 className="text-lg font-semibold mb-4">Detail Publikasi</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="publisher">Penerbit *</Label>
|
||||||
|
<Input
|
||||||
|
id="publisher"
|
||||||
|
value={formData.publisher}
|
||||||
|
onChange={(e) => handleInputChange("publisher", e.target.value)}
|
||||||
|
placeholder="Masukkan nama penerbit"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="publishedYear">Tahun Terbit *</Label>
|
||||||
|
<Input
|
||||||
|
id="publishedYear"
|
||||||
|
type="number"
|
||||||
|
value={formData.publishedYear}
|
||||||
|
onChange={(e) => handleInputChange("publishedYear", parseInt(e.target.value))}
|
||||||
|
min="1900"
|
||||||
|
max="2100"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="pageCount">Jumlah Halaman *</Label>
|
||||||
|
<Input
|
||||||
|
id="pageCount"
|
||||||
|
type="number"
|
||||||
|
value={formData.pageCount}
|
||||||
|
onChange={(e) => handleInputChange("pageCount", parseInt(e.target.value))}
|
||||||
|
min="1"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="language">Bahasa *</Label>
|
||||||
|
<Input
|
||||||
|
id="language"
|
||||||
|
value={formData.language}
|
||||||
|
onChange={(e) => handleInputChange("language", e.target.value)}
|
||||||
|
placeholder="Indonesia"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="isbn">ISBN</Label>
|
||||||
|
<Input
|
||||||
|
id="isbn"
|
||||||
|
value={formData.isbn}
|
||||||
|
onChange={(e) => handleInputChange("isbn", e.target.value)}
|
||||||
|
placeholder="Masukkan ISBN"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="price">Harga (IDR) *</Label>
|
||||||
|
<Input
|
||||||
|
id="price"
|
||||||
|
type="number"
|
||||||
|
value={formData.price}
|
||||||
|
onChange={(e) => handleInputChange("price", parseInt(e.target.value))}
|
||||||
|
min="0"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* File Upload */}
|
||||||
|
<div className="bg-white p-6 rounded-lg border">
|
||||||
|
<h2 className="text-lg font-semibold mb-4">Upload File</h2>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="pdfFile">File PDF Ebook</Label>
|
||||||
|
<Input
|
||||||
|
id="pdfFile"
|
||||||
|
type="file"
|
||||||
|
accept=".pdf"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
className="mt-2"
|
||||||
|
/>
|
||||||
|
{selectedFile && (
|
||||||
|
<p className="text-sm text-gray-600 mt-2">
|
||||||
|
File terpilih: {selectedFile.name}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status */}
|
||||||
|
<div className="bg-white p-6 rounded-lg border">
|
||||||
|
<h2 className="text-lg font-semibold mb-4">Status</h2>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id="isPublished"
|
||||||
|
checked={formData.isPublished}
|
||||||
|
onCheckedChange={(checked) => handleInputChange("isPublished", checked)}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="isPublished">Publikasikan ebook</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Submit Button */}
|
||||||
|
<div className="flex justify-end space-x-4">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading || uploadingFile}
|
||||||
|
>
|
||||||
|
{loading ? "Menyimpan..." : uploadingFile ? "Mengupload file..." : mode === "create" ? "Buat Ebook" : "Perbarui Ebook"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -10,6 +10,7 @@ import Cookies from "js-cookie";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { error } from "@/config/swal";
|
import { error } from "@/config/swal";
|
||||||
import { EyeSlashFilledIcon, EyeFilledIcon } from "../icons";
|
import { EyeSlashFilledIcon, EyeFilledIcon } from "../icons";
|
||||||
|
import { getProfile, postSignIn } from "@/service/master-user";
|
||||||
|
|
||||||
// Dummy akun
|
// Dummy akun
|
||||||
const dummyAccounts = [
|
const dummyAccounts = [
|
||||||
|
|
@ -47,58 +48,58 @@ export default function Login() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
if (!username || !password) {
|
// if (!username || !password) {
|
||||||
error("Username & Password Wajib Diisi !");
|
// error("Username & Password Wajib Diisi !");
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
// cek akun dummy dulu
|
// // cek akun dummy dulu
|
||||||
const dummyUser = dummyAccounts.find(
|
// const dummyUser = dummyAccounts.find(
|
||||||
(acc) => acc.username === username && acc.password === password
|
// (acc) => acc.username === username && acc.password === password
|
||||||
);
|
// );
|
||||||
|
|
||||||
if (dummyUser) {
|
// if (dummyUser) {
|
||||||
// set cookies sesuai role dummy
|
// // set cookies sesuai role dummy
|
||||||
Cookies.set("username", dummyUser.username, { expires: 1 });
|
// Cookies.set("username", dummyUser.username, { expires: 1 });
|
||||||
Cookies.set("ufne", dummyUser.fullname, { expires: 1 });
|
// Cookies.set("ufne", dummyUser.fullname, { expires: 1 });
|
||||||
Cookies.set("urie", dummyUser.userRoleId, { expires: 1 });
|
// Cookies.set("urie", dummyUser.userRoleId, { expires: 1 });
|
||||||
Cookies.set("status", "login", { expires: 1 });
|
// Cookies.set("status", "login", { expires: 1 });
|
||||||
|
|
||||||
if (dummyUser.username === "admin") {
|
// if (dummyUser.username === "admin") {
|
||||||
router.push("/admin/management-ebook");
|
// router.push("/admin/management-ebook");
|
||||||
} else {
|
// } else {
|
||||||
router.push("/admin/dashboard");
|
// router.push("/admin/dashboard");
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
// kalau tidak cocok dengan dummy akun → fallback ke API asli
|
// kalau tidak cocok dengan dummy akun → fallback ke API asli
|
||||||
// const response = await postSignIn({ username, password });
|
const response = await postSignIn({ username, password });
|
||||||
// if (response?.error) {
|
if (response?.error) {
|
||||||
// error("Username / Password Tidak Sesuai");
|
error("Username / Password Tidak Sesuai");
|
||||||
// } else {
|
} else {
|
||||||
// const profile = await getProfile(response?.data?.data?.access_token);
|
const profile = await getProfile(response?.data?.data?.access_token);
|
||||||
// Cookies.set("access_token", response?.data?.data?.access_token, {
|
Cookies.set("access_token", response?.data?.data?.access_token, {
|
||||||
// expires: 1,
|
expires: 1,
|
||||||
// });
|
});
|
||||||
// Cookies.set("refresh_token", response?.data?.data?.refresh_token, {
|
Cookies.set("refresh_token", response?.data?.data?.refresh_token, {
|
||||||
// expires: 1,
|
expires: 1,
|
||||||
// });
|
});
|
||||||
// Cookies.set("uid", profile?.data?.data?.keycloakId, { expires: 1 });
|
Cookies.set("uid", profile?.data?.data?.keycloakId, { expires: 1 });
|
||||||
// Cookies.set("uie", profile?.data?.data?.id, { expires: 1 });
|
Cookies.set("uie", profile?.data?.data?.id, { expires: 1 });
|
||||||
// Cookies.set("urie", profile?.data?.data?.userRoleId, { expires: 1 });
|
Cookies.set("urie", profile?.data?.data?.userRoleId, { expires: 1 });
|
||||||
// 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");
|
router.push("/admin/dashboard");
|
||||||
// }
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen">
|
<div className="flex h-screen">
|
||||||
{/* Left side - Form */}
|
{/* Left side - Form */}
|
||||||
<div className="w-full md:w-1/2 flex flex-col justify-center items-center px-6">
|
<div className="w-full lg:w-1/2 flex flex-col justify-center items-center px-6">
|
||||||
<div className="w-full max-w-md space-y-6">
|
<div className="w-full max-w-md space-y-6">
|
||||||
<div className="flex justify-start">
|
<div className="flex justify-center lg:justify-start">
|
||||||
<Link href={"/"}>
|
<Link href={"/"}>
|
||||||
<Image src="/narasi.png" alt="Logo" width={150} height={150} />
|
<Image src="/narasi.png" alt="Logo" width={150} height={150} />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
@ -139,8 +140,8 @@ export default function Login() {
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center text-sm">
|
<div className="flex justify-between items-center text-sm">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Checkbox id="ingat" />
|
{/* <Checkbox id="ingat" />
|
||||||
<label htmlFor="ingat">Ingat saya</label>
|
<label htmlFor="ingat">Ingat saya</label> */}
|
||||||
</div>
|
</div>
|
||||||
<Link href="#" className="text-gray-500 hover:underline">
|
<Link href="#" className="text-gray-500 hover:underline">
|
||||||
Lupa Kata Sandi?
|
Lupa Kata Sandi?
|
||||||
|
|
@ -169,7 +170,7 @@ export default function Login() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right side - Background & Testimonial */}
|
{/* Right side - Background & Testimonial */}
|
||||||
<div className="hidden md:flex w-1/2 relative">
|
<div className="hidden lg:flex w-1/2 relative">
|
||||||
<Image
|
<Image
|
||||||
src="/bg-auth.png"
|
src="/bg-auth.png"
|
||||||
alt="Background"
|
alt="Background"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,408 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { ArrowLeft, Upload, FileText, Video, Music } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { getAllUsers } from "@/service/user";
|
||||||
|
import {
|
||||||
|
createChatSessionId,
|
||||||
|
createSchedule,
|
||||||
|
uploadScheduleFiles,
|
||||||
|
} from "@/service/schedule";
|
||||||
|
import { toast } from "react-hot-toast";
|
||||||
|
import { getChatHistory } from "@/lib/chatHistory";
|
||||||
|
import Cookies from "js-cookie";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id: number;
|
||||||
|
fullname: string;
|
||||||
|
email: string;
|
||||||
|
user_role_id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ScheduleFormData {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
summary: string;
|
||||||
|
scheduled_at: string;
|
||||||
|
duration: number;
|
||||||
|
selectedUsers: number[];
|
||||||
|
discussionPoints: string;
|
||||||
|
journalFile: File | null;
|
||||||
|
videoFile: File | null;
|
||||||
|
audioFile: File | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ScheduleForm() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [users, setUsers] = useState<User[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [formData, setFormData] = useState<ScheduleFormData>({
|
||||||
|
title: "",
|
||||||
|
description: "",
|
||||||
|
summary: "",
|
||||||
|
scheduled_at: "",
|
||||||
|
duration: 60,
|
||||||
|
selectedUsers: [],
|
||||||
|
discussionPoints: "",
|
||||||
|
journalFile: null,
|
||||||
|
videoFile: null,
|
||||||
|
audioFile: null,
|
||||||
|
});
|
||||||
|
const uid = Cookies.get("uid");
|
||||||
|
const userId = Cookies.get("uie");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadUsers();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadUsers = async () => {
|
||||||
|
try {
|
||||||
|
const response = await getAllUsers();
|
||||||
|
if (!response.error && response.data?.data) {
|
||||||
|
setUsers(response.data.data);
|
||||||
|
} else {
|
||||||
|
toast.error("Gagal memuat data users");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Terjadi kesalahan saat memuat data users");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUserToggle = (userId: number) => {
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
selectedUsers: prev.selectedUsers.includes(userId)
|
||||||
|
? prev.selectedUsers.filter((id) => id !== userId)
|
||||||
|
: [...prev.selectedUsers, userId],
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileChange = (
|
||||||
|
fileType: "journal" | "video" | "audio",
|
||||||
|
file: File | null
|
||||||
|
) => {
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[`${fileType}File`]: file,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!formData.title || !formData.description || !formData.scheduled_at) {
|
||||||
|
toast.error("Mohon lengkapi semua field yang wajib diisi");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formData.selectedUsers.length === 0) {
|
||||||
|
toast.error("Pilih minimal satu user untuk dijadwalkan");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create schedule
|
||||||
|
const requestSessionId = {
|
||||||
|
name: formData.title,
|
||||||
|
type: "personal",
|
||||||
|
userIds: formData.selectedUsers,
|
||||||
|
};
|
||||||
|
const result = await createChatSessionId(requestSessionId);
|
||||||
|
const chatSessionId = result.data.id;
|
||||||
|
const scheduleData = {
|
||||||
|
title: formData.title,
|
||||||
|
description: formData.description,
|
||||||
|
summary: formData.summary,
|
||||||
|
scheduled_at: new Date(formData.scheduled_at).toISOString(),
|
||||||
|
duration: formData.duration, // Convert minutes to seconds
|
||||||
|
chat_session_id: chatSessionId,
|
||||||
|
};
|
||||||
|
const scheduleResponse = await createSchedule(scheduleData);
|
||||||
|
|
||||||
|
if (scheduleResponse.error) {
|
||||||
|
toast.error("Gagal membuat jadwal: " + scheduleResponse.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scheduleId = scheduleResponse.data?.id;
|
||||||
|
console.log("font", scheduleResponse.data);
|
||||||
|
|
||||||
|
if (!scheduleId) {
|
||||||
|
toast.error("Gagal mendapatkan ID jadwal");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload files if any
|
||||||
|
const filesToUpload = [
|
||||||
|
formData.journalFile,
|
||||||
|
formData.videoFile,
|
||||||
|
formData.audioFile,
|
||||||
|
].filter((file) => file !== null) as File[];
|
||||||
|
|
||||||
|
if (filesToUpload.length > 0) {
|
||||||
|
try {
|
||||||
|
await uploadScheduleFiles(scheduleId, filesToUpload);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error uploading files:", error);
|
||||||
|
toast.error("Gagal mengupload file");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success("Jadwal berhasil dibuat!");
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
setFormData({
|
||||||
|
title: "",
|
||||||
|
description: "",
|
||||||
|
summary: "",
|
||||||
|
scheduled_at: "",
|
||||||
|
duration: 60,
|
||||||
|
selectedUsers: [],
|
||||||
|
discussionPoints: "",
|
||||||
|
journalFile: null,
|
||||||
|
videoFile: null,
|
||||||
|
audioFile: null,
|
||||||
|
});
|
||||||
|
router.push("/admin/schedule");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating schedule:", error);
|
||||||
|
toast.error("Terjadi kesalahan saat membuat jadwal");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-4xl mx-auto space-y-6">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Link href="/admin/schedule">
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||||
|
Kembali
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<h1 className="text-2xl font-bold">Buat Jadwal Baru</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Informasi Jadwal</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="title">Judul Jadwal *</Label>
|
||||||
|
<Input
|
||||||
|
id="title"
|
||||||
|
value={formData.title}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormData((prev) => ({ ...prev, title: e.target.value }))
|
||||||
|
}
|
||||||
|
placeholder="Masukkan judul jadwal"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="duration">Durasi (menit) *</Label>
|
||||||
|
<Input
|
||||||
|
id="duration"
|
||||||
|
type="number"
|
||||||
|
value={formData.duration}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
duration: parseInt(e.target.value) || 60,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
min="1"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="scheduled_at">Tanggal dan Waktu *</Label>
|
||||||
|
<Input
|
||||||
|
id="scheduled_at"
|
||||||
|
type="datetime-local"
|
||||||
|
value={formData.scheduled_at}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
scheduled_at: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="description">Deskripsi *</Label>
|
||||||
|
<Textarea
|
||||||
|
id="description"
|
||||||
|
value={formData.description}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
description: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
placeholder="Masukkan deskripsi jadwal"
|
||||||
|
rows={3}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="summary">Ringkasan</Label>
|
||||||
|
<Textarea
|
||||||
|
id="summary"
|
||||||
|
value={formData.summary}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormData((prev) => ({ ...prev, summary: e.target.value }))
|
||||||
|
}
|
||||||
|
placeholder="Masukkan ringkasan jadwal"
|
||||||
|
rows={2}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="discussionPoints">Poin Pembahasan</Label>
|
||||||
|
<Textarea
|
||||||
|
id="discussionPoints"
|
||||||
|
value={formData.discussionPoints}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
discussionPoints: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
placeholder="Masukkan poin-poin yang akan dibahas"
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Pilih User untuk Dijadwalkan</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-3 max-h-60 overflow-y-auto">
|
||||||
|
{users.map((user) => (
|
||||||
|
<div key={user.id} className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id={`user-${user.id}`}
|
||||||
|
checked={formData.selectedUsers.includes(user.id)}
|
||||||
|
onCheckedChange={() => handleUserToggle(user.id)}
|
||||||
|
/>
|
||||||
|
<Label htmlFor={`user-${user.id}`} className="flex-1">
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">{user.fullname}</div>
|
||||||
|
<div className="text-sm text-gray-500">{user.email}</div>
|
||||||
|
</div>
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{formData.selectedUsers.length > 0 && (
|
||||||
|
<p className="text-sm text-gray-600 mt-2">
|
||||||
|
{formData.selectedUsers.length} user dipilih
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Upload File</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="journalFile">File Jurnal (PDF)</Label>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Input
|
||||||
|
id="journalFile"
|
||||||
|
type="file"
|
||||||
|
accept=".pdf"
|
||||||
|
onChange={(e) =>
|
||||||
|
handleFileChange("journal", e.target.files?.[0] || null)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<FileText className="w-5 h-5 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
{formData.journalFile && (
|
||||||
|
<p className="text-sm text-green-600 mt-1">
|
||||||
|
File dipilih: {formData.journalFile.name}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="videoFile">File Video (MP4, dll)</Label>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Input
|
||||||
|
id="videoFile"
|
||||||
|
type="file"
|
||||||
|
accept="video/*"
|
||||||
|
onChange={(e) =>
|
||||||
|
handleFileChange("video", e.target.files?.[0] || null)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Video className="w-5 h-5 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
{formData.videoFile && (
|
||||||
|
<p className="text-sm text-green-600 mt-1">
|
||||||
|
File dipilih: {formData.videoFile.name}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="audioFile">File Audio</Label>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Input
|
||||||
|
id="audioFile"
|
||||||
|
type="file"
|
||||||
|
accept="audio/*"
|
||||||
|
onChange={(e) =>
|
||||||
|
handleFileChange("audio", e.target.files?.[0] || null)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Music className="w-5 h-5 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
{formData.audioFile && (
|
||||||
|
<p className="text-sm text-green-600 mt-1">
|
||||||
|
File dipilih: {formData.audioFile.name}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-4">
|
||||||
|
<Link href="/admin/schedule">
|
||||||
|
<Button type="button" variant="outline">
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<Button type="submit" disabled={loading}>
|
||||||
|
{loading ? "Menyimpan..." : "Simpan Jadwal"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -3,23 +3,105 @@ import Image from "next/image";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
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 { EyeFilledIcon, EyeSlashFilledIcon } from "../icons";
|
||||||
|
|
||||||
|
const formSchema = z
|
||||||
|
.object({
|
||||||
|
fullname: z.string().min(2, {
|
||||||
|
message: "Nama Lengkap wajib diisi",
|
||||||
|
}),
|
||||||
|
phone: z.string().min(2, {
|
||||||
|
message: "No. Handphone/Whatsapp wajib diisi",
|
||||||
|
}),
|
||||||
|
email: z.string().email({
|
||||||
|
message: "Alamat email wajib valid",
|
||||||
|
}),
|
||||||
|
password: z.string().min(8, {
|
||||||
|
message: "Kata Sandi minimal 8 karakter",
|
||||||
|
}),
|
||||||
|
passwordConfirm: z.string().min(8, {
|
||||||
|
message: "Konfirmasi Kata Sandi minimal 8 karakter",
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.refine((data) => data.password === data.passwordConfirm, {
|
||||||
|
message: "Konfirmasi kata sandi tidak sama",
|
||||||
|
path: ["passwordConfirm"],
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormData = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
export default function SignUp() {
|
export default function SignUp() {
|
||||||
|
const MySwal = withReactContent(Swal);
|
||||||
const [role, setRole] = useState<"pengguna" | "tenagaAhli">("pengguna");
|
const [role, setRole] = useState<"pengguna" | "tenagaAhli">("pengguna");
|
||||||
|
const [isVisible, setIsVisible] = useState([false, false]);
|
||||||
|
const [lastJob, setLastJob] = useState("");
|
||||||
|
const [lastEducation, setLastEducation] = useState("");
|
||||||
|
const [roleValidation, setRoleValidation] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm<FormData>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
fullname: "",
|
||||||
|
phone: "",
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
passwordConfirm: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = (data: FormData) => {
|
||||||
|
const validation: string[] = [];
|
||||||
|
|
||||||
|
if (role === "tenagaAhli") {
|
||||||
|
if (!lastEducation) validation.push("lastEducation");
|
||||||
|
if (!lastJob) validation.push("lastJob");
|
||||||
|
}
|
||||||
|
|
||||||
|
setRoleValidation(validation);
|
||||||
|
|
||||||
|
if (validation.length === 0) {
|
||||||
|
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 = (data: FormData) => {
|
||||||
|
console.log({ ...data, role });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen">
|
<div className="flex h-screen">
|
||||||
{/* Left side - Form */}
|
{/* Left side - Form */}
|
||||||
<div className="w-full md:w-1/2 flex flex-col justify-center items-center px-6">
|
<div className="w-full lg:w-1/2 flex flex-col justify-center items-center px-6">
|
||||||
<div className="w-full max-w-md space-y-6">
|
<div className="w-full max-w-md space-y-6">
|
||||||
<div className="flex justify-start">
|
<div className="flex justify-center lg:justify-start">
|
||||||
<Link href={"/"}>
|
<Link href={"/"}>
|
||||||
<Image src="/narasi.png" alt="Logo" width={150} height={150} />
|
<Image src="/narasi.png" alt="Logo" width={150} height={150} />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<form className="space-y-4">
|
|
||||||
{/* Judul */}
|
<form className="space-y-4" onSubmit={handleSubmit(onSubmit)}>
|
||||||
<h2 className="text-2xl font-semibold text-start">
|
<h2 className="text-2xl font-semibold text-start">
|
||||||
Senang Bertemu Kembali!
|
Senang Bertemu Kembali!
|
||||||
</h2>
|
</h2>
|
||||||
|
|
@ -29,7 +111,6 @@ export default function SignUp() {
|
||||||
<label className="flex items-center space-x-2">
|
<label className="flex items-center space-x-2">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
name="role"
|
|
||||||
value="pengguna"
|
value="pengguna"
|
||||||
checked={role === "pengguna"}
|
checked={role === "pengguna"}
|
||||||
onChange={() => setRole("pengguna")}
|
onChange={() => setRole("pengguna")}
|
||||||
|
|
@ -39,7 +120,6 @@ export default function SignUp() {
|
||||||
<label className="flex items-center space-x-2">
|
<label className="flex items-center space-x-2">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
name="role"
|
|
||||||
value="tenagaAhli"
|
value="tenagaAhli"
|
||||||
checked={role === "tenagaAhli"}
|
checked={role === "tenagaAhli"}
|
||||||
onChange={() => setRole("tenagaAhli")}
|
onChange={() => setRole("tenagaAhli")}
|
||||||
|
|
@ -49,72 +129,149 @@ export default function SignUp() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Input Fields */}
|
{/* Input Fields */}
|
||||||
{role === "pengguna" ? (
|
<Controller
|
||||||
<>
|
control={control}
|
||||||
|
name="fullname"
|
||||||
|
render={({ field }) => (
|
||||||
<Input
|
<Input
|
||||||
|
{...field}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Nama Lengkap"
|
placeholder={
|
||||||
|
role === "pengguna"
|
||||||
|
? "Nama Lengkap"
|
||||||
|
: "Nama Lengkap & Gelar"
|
||||||
|
}
|
||||||
className="bg-white"
|
className="bg-white"
|
||||||
/>
|
/>
|
||||||
<Input
|
)}
|
||||||
type="tel"
|
/>
|
||||||
placeholder="No Handphone/Whatsapp"
|
{errors.fullname && (
|
||||||
className="bg-white"
|
<p className="text-red-500 text-sm">{errors.fullname.message}</p>
|
||||||
/>
|
)}
|
||||||
<Input
|
|
||||||
type="email"
|
{role === "tenagaAhli" && (
|
||||||
placeholder="Alamat email"
|
|
||||||
className="bg-white"
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
type="password"
|
|
||||||
placeholder="Kata sandi (min. 8 karakter)"
|
|
||||||
className="bg-white"
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
type="password"
|
|
||||||
placeholder="Konfirmasi Kata sandi"
|
|
||||||
className="bg-white"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
<>
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
placeholder="Nama Lengkap & Gelar"
|
|
||||||
className="bg-white"
|
|
||||||
/>
|
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Pendidikan Terakhir"
|
placeholder="Pendidikan Terakhir"
|
||||||
className="bg-white"
|
className="bg-white"
|
||||||
/>
|
value={lastEducation}
|
||||||
|
onChange={(e) => setLastEducation(e.target.value)}
|
||||||
|
/>{" "}
|
||||||
|
{roleValidation.includes("lastEducation") && !lastEducation && (
|
||||||
|
<p className="text-red-500 text-sm">
|
||||||
|
Pendidikan Terakhir wajib diisi
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Pekerjaan Terakhir"
|
placeholder="Pekerjaan Terakhir"
|
||||||
className="bg-white"
|
className="bg-white"
|
||||||
|
value={lastJob}
|
||||||
|
onChange={(e) => setLastJob(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
{roleValidation.includes("lastJob") && !lastJob && (
|
||||||
|
<p className="text-red-500 text-sm">
|
||||||
|
Pekerjaan Terakhir wajib diisi
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="phone"
|
||||||
|
render={({ field }) => (
|
||||||
<Input
|
<Input
|
||||||
type="tel"
|
{...field}
|
||||||
|
type="number"
|
||||||
placeholder="No Handphone/Whatsapp"
|
placeholder="No Handphone/Whatsapp"
|
||||||
className="bg-white"
|
className="bg-white"
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{errors.phone && (
|
||||||
|
<p className="text-red-500 text-sm">{errors.phone.message}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="email"
|
||||||
|
render={({ field }) => (
|
||||||
<Input
|
<Input
|
||||||
|
{...field}
|
||||||
type="email"
|
type="email"
|
||||||
placeholder="Alamat email"
|
placeholder="Alamat email"
|
||||||
className="bg-white"
|
className="bg-white"
|
||||||
/>
|
/>
|
||||||
<Input
|
)}
|
||||||
type="password"
|
/>
|
||||||
placeholder="Kata sandi (min. 8 karakter)"
|
{errors.email && (
|
||||||
className="bg-white"
|
<p className="text-red-500 text-sm">{errors.email.message}</p>
|
||||||
/>
|
)}
|
||||||
<Input
|
|
||||||
type="password"
|
<Controller
|
||||||
placeholder="Konfirmasi Kata sandi"
|
control={control}
|
||||||
className="bg-white"
|
name="password"
|
||||||
/>
|
render={({ field }) => (
|
||||||
</>
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
id="password"
|
||||||
|
required
|
||||||
|
type={isVisible[0] ? "text" : "password"}
|
||||||
|
placeholder="Masukkan password"
|
||||||
|
className="w-full px-4 py-3 pr-12 border border-gray-300 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 transition-colors"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsVisible([!isVisible[0], isVisible[1]])}
|
||||||
|
className="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600 focus:outline-none transition-colors"
|
||||||
|
>
|
||||||
|
{isVisible[0] ? (
|
||||||
|
<EyeSlashFilledIcon className="w-5 h-5" />
|
||||||
|
) : (
|
||||||
|
<EyeFilledIcon className="w-5 h-5" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{errors.password && (
|
||||||
|
<p className="text-red-500 text-sm">{errors.password.message}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="passwordConfirm"
|
||||||
|
render={({ field }) => (
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
id="password"
|
||||||
|
required
|
||||||
|
type={isVisible[1] ? "text" : "password"}
|
||||||
|
placeholder="Masukkan password"
|
||||||
|
className="w-full px-4 py-3 pr-12 border border-gray-300 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 transition-colors"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsVisible([isVisible[0], !isVisible[1]])}
|
||||||
|
className="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600 focus:outline-none transition-colors"
|
||||||
|
>
|
||||||
|
{isVisible[1] ? (
|
||||||
|
<EyeSlashFilledIcon className="w-5 h-5" />
|
||||||
|
) : (
|
||||||
|
<EyeFilledIcon className="w-5 h-5" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{errors.passwordConfirm && (
|
||||||
|
<p className="text-red-500 text-sm">
|
||||||
|
{errors.passwordConfirm.message}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Submit Button */}
|
{/* Submit Button */}
|
||||||
|
|
@ -136,8 +293,8 @@ export default function SignUp() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right side - Background & Testimonial */}
|
{/* Right side - Background */}
|
||||||
<div className="hidden md:flex w-1/2 relative">
|
<div className="hidden lg:flex w-1/2 relative">
|
||||||
<Image
|
<Image
|
||||||
src="/bg-auth.png"
|
src="/bg-auth.png"
|
||||||
alt="Background"
|
alt="Background"
|
||||||
|
|
@ -145,9 +302,9 @@ export default function SignUp() {
|
||||||
objectFit="cover"
|
objectFit="cover"
|
||||||
className="z-1"
|
className="z-1"
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 flex flex-col justify-center px-16 text-white z-10">
|
<div className="absolute inset-0 flex flex-col justify-center px-16 text-white z-10">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<p className="text-6xl bg-[#27272A] w-[60px] h-[65px] p-3 rounded-2xl">
|
<p className="text-6xl bg-[#27272A] w-[60px] h-[65px] p-3 rounded-2xl">
|
||||||
❝
|
❝
|
||||||
</p>
|
</p>
|
||||||
<p className="text-3xl leading-relaxed w-8/12">
|
<p className="text-3xl leading-relaxed w-8/12">
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,385 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { getUserDetail, getUserWorkHistory, getUserEducationHistory } from "@/service/user";
|
||||||
|
import { toast } from "react-hot-toast";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Edit, User, Mail, Phone, MapPin, Calendar, GraduationCap, Briefcase, Clock } from "lucide-react";
|
||||||
|
|
||||||
|
interface UserDetailProps {
|
||||||
|
userId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WorkHistory {
|
||||||
|
id: number;
|
||||||
|
job_title: string;
|
||||||
|
company_name: string;
|
||||||
|
start_date: string;
|
||||||
|
end_date: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EducationHistory {
|
||||||
|
id: number;
|
||||||
|
school_name: string;
|
||||||
|
major: string;
|
||||||
|
education_level: string;
|
||||||
|
graduation_year: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UserDetail({ userId }: UserDetailProps) {
|
||||||
|
const router = useRouter();
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [userData, setUserData] = useState<any>(null);
|
||||||
|
const [workHistory, setWorkHistory] = useState<WorkHistory[]>([]);
|
||||||
|
const [educationHistory, setEducationHistory] = useState<EducationHistory[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
loadUserData();
|
||||||
|
}, [userId]);
|
||||||
|
|
||||||
|
const loadUserData = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await getUserDetail(userId);
|
||||||
|
if (response.success) {
|
||||||
|
setUserData(response.data);
|
||||||
|
|
||||||
|
// Load work and education history if user is tenaga ahli
|
||||||
|
if (response.data.user_role_id === 2) {
|
||||||
|
const workResponse = await getUserWorkHistory(userId);
|
||||||
|
const educationResponse = await getUserEducationHistory(userId);
|
||||||
|
|
||||||
|
if (!workResponse.error && workResponse.data?.data) {
|
||||||
|
setWorkHistory(workResponse.data.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!educationResponse.error && educationResponse.data?.data) {
|
||||||
|
setEducationHistory(educationResponse.data.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast.error("Gagal memuat data user");
|
||||||
|
router.push("/admin/management-user");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Terjadi kesalahan saat memuat data user");
|
||||||
|
router.push("/admin/management-user");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateString: string) => {
|
||||||
|
return new Date(dateString).toLocaleDateString('id-ID', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDateTime = (dateString: string) => {
|
||||||
|
return new Date(dateString).toLocaleDateString('id-ID', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUserRoleName = (roleId: number) => {
|
||||||
|
switch (roleId) {
|
||||||
|
case 2:
|
||||||
|
return "Tenaga Ahli";
|
||||||
|
case 3:
|
||||||
|
return "Pengguna Umum";
|
||||||
|
default:
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUserLevelName = (levelId: number) => {
|
||||||
|
switch (levelId) {
|
||||||
|
case 1:
|
||||||
|
return "Basic";
|
||||||
|
case 2:
|
||||||
|
return "Expert";
|
||||||
|
default:
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center h-64">
|
||||||
|
<div className="text-lg">Loading...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center h-64">
|
||||||
|
<div className="text-lg">Memuat data user...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!userData) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center h-64">
|
||||||
|
<div className="text-lg text-red-500">Data user tidak ditemukan</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-6xl mx-auto space-y-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-semibold">{userData.fullname}</h1>
|
||||||
|
<p className="text-gray-600">Detail informasi user</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<Link href={`/admin/management-user/edit/${userId}`}>
|
||||||
|
<Button variant="outline" className="flex items-center space-x-2">
|
||||||
|
<Edit className="w-4 h-4" />
|
||||||
|
<span>Edit</span>
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<Button variant="outline" onClick={() => router.back()}>
|
||||||
|
Kembali
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
|
{/* Main Content */}
|
||||||
|
<div className="lg:col-span-2 space-y-6">
|
||||||
|
{/* Basic Information */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center space-x-2">
|
||||||
|
<User className="w-5 h-5" />
|
||||||
|
<span>Informasi Dasar</span>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">Nama Lengkap</label>
|
||||||
|
<p className="text-lg font-semibold">{userData.fullname}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">Username</label>
|
||||||
|
<p className="text-sm font-mono bg-gray-100 p-2 rounded">{userData.username}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">Email</label>
|
||||||
|
<p className="text-sm flex items-center space-x-2">
|
||||||
|
<Mail className="w-4 h-4" />
|
||||||
|
<span>{userData.email}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">No Telepon</label>
|
||||||
|
<p className="text-sm flex items-center space-x-2">
|
||||||
|
<Phone className="w-4 h-4" />
|
||||||
|
<span>{userData.phone_number}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">No Whatsapp</label>
|
||||||
|
<p className="text-sm flex items-center space-x-2">
|
||||||
|
<Phone className="w-4 h-4" />
|
||||||
|
<span>{userData.whatsapp_number}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">Tanggal Lahir</label>
|
||||||
|
<p className="text-sm flex items-center space-x-2">
|
||||||
|
<Calendar className="w-4 h-4" />
|
||||||
|
<span>{formatDate(userData.date_of_birth)}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">Jenis Kelamin</label>
|
||||||
|
<p className="text-sm capitalize">{userData.gender_type}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">Alamat</label>
|
||||||
|
<p className="text-sm flex items-center space-x-2">
|
||||||
|
<MapPin className="w-4 h-4" />
|
||||||
|
<span>{userData.address}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{userData.degree && (
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">Gelar</label>
|
||||||
|
<p className="text-sm">{userData.degree}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Education History */}
|
||||||
|
{userData.user_role_id === 2 && educationHistory.length > 0 && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center space-x-2">
|
||||||
|
<GraduationCap className="w-5 h-5" />
|
||||||
|
<span>Riwayat Pendidikan</span>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{educationHistory.map((education) => (
|
||||||
|
<div key={education.id} className="border rounded-lg p-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">Nama Sekolah/Universitas</label>
|
||||||
|
<p className="text-sm font-semibold">{education.school_name}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">Jurusan</label>
|
||||||
|
<p className="text-sm">{education.major}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">Tingkat Pendidikan</label>
|
||||||
|
<p className="text-sm">{education.education_level}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">Tahun Lulus</label>
|
||||||
|
<p className="text-sm">{education.graduation_year}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Work History */}
|
||||||
|
{userData.user_role_id === 2 && workHistory.length > 0 && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center space-x-2">
|
||||||
|
<Briefcase className="w-5 h-5" />
|
||||||
|
<span>Riwayat Pekerjaan</span>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{workHistory.map((work) => (
|
||||||
|
<div key={work.id} className="border rounded-lg p-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">Jabatan</label>
|
||||||
|
<p className="text-sm font-semibold">{work.job_title}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">Perusahaan</label>
|
||||||
|
<p className="text-sm">{work.company_name}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">Tanggal Mulai</label>
|
||||||
|
<p className="text-sm flex items-center space-x-2">
|
||||||
|
<Calendar className="w-4 h-4" />
|
||||||
|
<span>{formatDate(work.start_date)}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">Tanggal Selesai</label>
|
||||||
|
<p className="text-sm flex items-center space-x-2">
|
||||||
|
<Calendar className="w-4 h-4" />
|
||||||
|
<span>{formatDate(work.end_date)}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sidebar */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* User Status */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Status User</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-sm text-gray-500">Status</span>
|
||||||
|
<Badge variant={userData.is_active ? "default" : "secondary"}>
|
||||||
|
{userData.is_active ? "Aktif" : "Tidak Aktif"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-sm text-gray-500">Jenis Akun</span>
|
||||||
|
<span className="font-semibold">{getUserRoleName(userData.user_role_id)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-sm text-gray-500">Level</span>
|
||||||
|
<span className="font-semibold">{getUserLevelName(userData.user_level_id)}</span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Account Information */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Informasi Akun</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<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">
|
||||||
|
{userData.keycloak_id}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-sm text-gray-500">Email Updated</span>
|
||||||
|
<span className="font-semibold">
|
||||||
|
{userData.is_email_updated ? "Ya" : "Tidak"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Timestamps */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center space-x-2">
|
||||||
|
<Clock className="w-5 h-5" />
|
||||||
|
<span>Timeline</span>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">Dibuat</label>
|
||||||
|
<p className="text-sm mt-1">{formatDateTime(userData.created_at)}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-500">Diperbarui</label>
|
||||||
|
<p className="text-sm mt-1">{formatDateTime(userData.updated_at)}</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,553 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
|
import {
|
||||||
|
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";
|
||||||
|
|
||||||
|
interface UserFormProps {
|
||||||
|
userId?: number;
|
||||||
|
mode: "create" | "edit";
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EducationItem {
|
||||||
|
schoolName: string;
|
||||||
|
major: string;
|
||||||
|
educationLevel: string;
|
||||||
|
graduationYear: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WorkItem {
|
||||||
|
jobTitle: string;
|
||||||
|
companyName: string;
|
||||||
|
startDate: string;
|
||||||
|
endDate: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UserForm({ userId, mode }: UserFormProps) {
|
||||||
|
const router = useRouter();
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [userType, setUserType] = useState<"tenaga_ahli" | "pengguna_umum">(
|
||||||
|
"tenaga_ahli"
|
||||||
|
);
|
||||||
|
const [educationList, setEducationList] = useState<EducationItem[]>([
|
||||||
|
{ schoolName: "", major: "", educationLevel: "", graduationYear: "" },
|
||||||
|
]);
|
||||||
|
const [workList, setWorkList] = useState<WorkItem[]>([
|
||||||
|
{ jobTitle: "", companyName: "", startDate: "", endDate: "" },
|
||||||
|
]);
|
||||||
|
const [formData, setFormData] = useState<UserData>({
|
||||||
|
username: "",
|
||||||
|
email: "",
|
||||||
|
fullname: "",
|
||||||
|
address: "",
|
||||||
|
phoneNumber: "",
|
||||||
|
whatsappNumber: "",
|
||||||
|
password: "",
|
||||||
|
dateOfBirth: "",
|
||||||
|
genderType: "male",
|
||||||
|
degree: "",
|
||||||
|
userLevelId: 1,
|
||||||
|
userRoleId: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle mounting and load user data for edit mode
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
if (mode === "edit" && userId) {
|
||||||
|
loadUserData();
|
||||||
|
}
|
||||||
|
}, [userId, mode]);
|
||||||
|
|
||||||
|
const loadUserData = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await getUserDetail(userId!);
|
||||||
|
if (response.success) {
|
||||||
|
const data = response.data;
|
||||||
|
setFormData({
|
||||||
|
username: data.username,
|
||||||
|
email: data.email,
|
||||||
|
fullname: data.fullname,
|
||||||
|
address: data.address,
|
||||||
|
phoneNumber: data.phoneNumber,
|
||||||
|
whatsappNumber: data.whatsappNumber,
|
||||||
|
password: "",
|
||||||
|
dateOfBirth: data.dateOfBirth,
|
||||||
|
genderType: data.genderType as "male" | "female",
|
||||||
|
degree: data.degree || "",
|
||||||
|
userLevelId: data.user_level_id,
|
||||||
|
userRoleId: data.user_role_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set user type based on role
|
||||||
|
setUserType(data.user_role_id === 2 ? "tenaga_ahli" : "pengguna_umum");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Gagal memuat data user");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (field: keyof UserData, value: any) => {
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[field]: value,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddEducation = () => {
|
||||||
|
setEducationList([
|
||||||
|
...educationList,
|
||||||
|
{ schoolName: "", major: "", educationLevel: "", graduationYear: "" },
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddWork = () => {
|
||||||
|
setWorkList([
|
||||||
|
...workList,
|
||||||
|
{ jobTitle: "", companyName: "", startDate: "", endDate: "" },
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEducationChange = (
|
||||||
|
index: number,
|
||||||
|
field: keyof EducationItem,
|
||||||
|
value: string
|
||||||
|
) => {
|
||||||
|
const updated = [...educationList];
|
||||||
|
updated[index][field] = value;
|
||||||
|
setEducationList(updated);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleWorkChange = (
|
||||||
|
index: number,
|
||||||
|
field: keyof WorkItem,
|
||||||
|
value: string
|
||||||
|
) => {
|
||||||
|
const updated = [...workList];
|
||||||
|
updated[index][field] = value;
|
||||||
|
setWorkList(updated);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// Set user role and level based on user type
|
||||||
|
const userData = {
|
||||||
|
...formData,
|
||||||
|
userRoleId: userType === "tenaga_ahli" ? 2 : 3,
|
||||||
|
userLevelId: userType === "tenaga_ahli" ? 2 : 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
let response;
|
||||||
|
if (mode === "create") {
|
||||||
|
response = await createUser(userData);
|
||||||
|
} else {
|
||||||
|
// Remove password from update if empty
|
||||||
|
const updateData = { ...userData };
|
||||||
|
if (!updateData.password) {
|
||||||
|
const { password, ...updateDataWithoutPassword } = updateData;
|
||||||
|
response = await updateUser(userId!, updateDataWithoutPassword);
|
||||||
|
} else {
|
||||||
|
response = await updateUser(userId!, updateData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response?.error) {
|
||||||
|
const userId = response?.data?.data?.id;
|
||||||
|
|
||||||
|
if (userId && userType === "tenaga_ahli") {
|
||||||
|
// Create education history
|
||||||
|
for (const education of educationList) {
|
||||||
|
if (
|
||||||
|
education.schoolName &&
|
||||||
|
education.major &&
|
||||||
|
education.educationLevel &&
|
||||||
|
education.graduationYear
|
||||||
|
) {
|
||||||
|
const educationData: EducationHistoryData = {
|
||||||
|
userId: userId,
|
||||||
|
schoolName: education.schoolName,
|
||||||
|
major: education.major,
|
||||||
|
educationLevel: education.educationLevel,
|
||||||
|
graduationYear: parseInt(education.graduationYear),
|
||||||
|
};
|
||||||
|
await createEducationHistory(educationData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create work history
|
||||||
|
for (const work of workList) {
|
||||||
|
if (
|
||||||
|
work.jobTitle &&
|
||||||
|
work.companyName &&
|
||||||
|
work.startDate &&
|
||||||
|
work.endDate
|
||||||
|
) {
|
||||||
|
const workData: WorkHistoryData = {
|
||||||
|
userId: userId,
|
||||||
|
jobTitle: work.jobTitle,
|
||||||
|
companyName: work.companyName,
|
||||||
|
startDate: work.startDate,
|
||||||
|
endDate: work.endDate,
|
||||||
|
};
|
||||||
|
await createWorkHistory(workData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success(
|
||||||
|
`User berhasil ${mode === "create" ? "dibuat" : "diperbarui"}`
|
||||||
|
);
|
||||||
|
router.push("/admin/management-user");
|
||||||
|
} else {
|
||||||
|
toast.error(
|
||||||
|
typeof response.message === "string"
|
||||||
|
? response.message
|
||||||
|
: "Terjadi kesalahan"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Terjadi kesalahan saat menyimpan user");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center h-64">
|
||||||
|
<div className="text-lg">Loading...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading && mode === "edit") {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center h-64">
|
||||||
|
<div className="text-lg">Memuat data...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-4xl mx-auto space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h1 className="text-2xl font-semibold">
|
||||||
|
{mode === "create" ? "Tambah User Baru" : "Edit User"}
|
||||||
|
</h1>
|
||||||
|
<Button variant="outline" onClick={() => router.back()}>
|
||||||
|
Kembali
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
{/* User Type Selection */}
|
||||||
|
<div className="bg-white p-6 rounded-lg border">
|
||||||
|
<h2 className="text-lg font-semibold mb-4">Jenis Pengguna</h2>
|
||||||
|
<RadioGroup
|
||||||
|
value={userType}
|
||||||
|
onValueChange={(value: "tenaga_ahli" | "pengguna_umum") =>
|
||||||
|
setUserType(value)
|
||||||
|
}
|
||||||
|
className="flex space-x-6"
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<RadioGroupItem value="tenaga_ahli" id="tenaga_ahli" />
|
||||||
|
<Label htmlFor="tenaga_ahli">Tenaga Ahli</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<RadioGroupItem value="pengguna_umum" id="pengguna_umum" />
|
||||||
|
<Label htmlFor="pengguna_umum">Pengguna Umum</Label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Basic Information */}
|
||||||
|
<div className="bg-white p-6 rounded-lg border">
|
||||||
|
<h2 className="text-lg font-semibold mb-4">Informasi Dasar</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="fullname">Nama Lengkap *</Label>
|
||||||
|
<Input
|
||||||
|
id="fullname"
|
||||||
|
value={formData.fullname}
|
||||||
|
onChange={(e) => handleInputChange("fullname", e.target.value)}
|
||||||
|
placeholder="Masukkan Nama Lengkap"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="username">Username *</Label>
|
||||||
|
<Input
|
||||||
|
id="username"
|
||||||
|
value={formData.username}
|
||||||
|
onChange={(e) => handleInputChange("username", e.target.value)}
|
||||||
|
placeholder="Masukkan Username"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{userType === "tenaga_ahli" && (
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="degree">Gelar</Label>
|
||||||
|
<Input
|
||||||
|
id="degree"
|
||||||
|
value={formData.degree}
|
||||||
|
onChange={(e) => handleInputChange("degree", e.target.value)}
|
||||||
|
placeholder="Masukkan Gelar"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="email">Email *</Label>
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
value={formData.email}
|
||||||
|
onChange={(e) => handleInputChange("email", e.target.value)}
|
||||||
|
placeholder="Masukkan Email"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="phoneNumber">No Telepon *</Label>
|
||||||
|
<Input
|
||||||
|
id="phoneNumber"
|
||||||
|
value={formData.phoneNumber}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleInputChange("phoneNumber", e.target.value)
|
||||||
|
}
|
||||||
|
placeholder="Masukkan No Telepon"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="whatsappNumber">No Whatsapp *</Label>
|
||||||
|
<Input
|
||||||
|
id="whatsappNumber"
|
||||||
|
value={formData.whatsappNumber}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleInputChange("whatsappNumber", e.target.value)
|
||||||
|
}
|
||||||
|
placeholder="Masukkan No Whatsapp"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="dateOfBirth">Tanggal Lahir *</Label>
|
||||||
|
<Input
|
||||||
|
id="dateOfBirth"
|
||||||
|
type="date"
|
||||||
|
value={formData.dateOfBirth}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleInputChange("dateOfBirth", e.target.value)
|
||||||
|
}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="genderType">Jenis Kelamin *</Label>
|
||||||
|
<Select
|
||||||
|
value={formData.genderType}
|
||||||
|
onValueChange={(value: "male" | "female") =>
|
||||||
|
handleInputChange("genderType", value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Pilih Jenis Kelamin" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="male">Laki-laki</SelectItem>
|
||||||
|
<SelectItem value="female">Perempuan</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="address">Alamat *</Label>
|
||||||
|
<Input
|
||||||
|
id="address"
|
||||||
|
value={formData.address}
|
||||||
|
onChange={(e) => handleInputChange("address", e.target.value)}
|
||||||
|
placeholder="Masukkan Alamat"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{mode === "create" && (
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="password">Kata Sandi *</Label>
|
||||||
|
<Input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
value={formData.password}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleInputChange("password", e.target.value)
|
||||||
|
}
|
||||||
|
placeholder="Masukkan Kata Sandi"
|
||||||
|
required={mode === "create"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Education History - only for Tenaga Ahli */}
|
||||||
|
{userType === "tenaga_ahli" && (
|
||||||
|
<div className="bg-white p-6 rounded-lg border">
|
||||||
|
<h2 className="text-lg font-semibold mb-4">Riwayat Pendidikan</h2>
|
||||||
|
{educationList.map((education, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="grid grid-cols-1 md:grid-cols-5 gap-4 mb-4"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder="Masukkan Nama Sekolah/Universitas"
|
||||||
|
value={education.schoolName}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleEducationChange(index, "schoolName", e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="Masukkan Jurusan"
|
||||||
|
value={education.major}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleEducationChange(index, "major", e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="Masukkan Tingkat Pendidikan"
|
||||||
|
value={education.educationLevel}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleEducationChange(
|
||||||
|
index,
|
||||||
|
"educationLevel",
|
||||||
|
e.target.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="Masukkan Tahun Lulus"
|
||||||
|
type="number"
|
||||||
|
value={education.graduationYear}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleEducationChange(
|
||||||
|
index,
|
||||||
|
"graduationYear",
|
||||||
|
e.target.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button variant="secondary" className="whitespace-nowrap">
|
||||||
|
+ Ijazah
|
||||||
|
</Button>
|
||||||
|
{index === educationList.length - 1 && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleAddEducation}
|
||||||
|
>
|
||||||
|
+ Tambah
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Work History - only for Tenaga Ahli */}
|
||||||
|
{userType === "tenaga_ahli" && (
|
||||||
|
<div className="bg-white p-6 rounded-lg border">
|
||||||
|
<h2 className="text-lg font-semibold mb-4">Riwayat Pekerjaan</h2>
|
||||||
|
{workList.map((work, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="grid grid-cols-1 md:grid-cols-5 gap-4 mb-4"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder="Masukkan Jabatan"
|
||||||
|
value={work.jobTitle}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleWorkChange(index, "jobTitle", e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="Masukkan Nama Perusahaan"
|
||||||
|
value={work.companyName}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleWorkChange(index, "companyName", e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="Tanggal Mulai"
|
||||||
|
type="date"
|
||||||
|
value={work.startDate}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleWorkChange(index, "startDate", e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="Tanggal Selesai"
|
||||||
|
type="date"
|
||||||
|
value={work.endDate}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleWorkChange(index, "endDate", e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{index === workList.length - 1 && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleAddWork}
|
||||||
|
>
|
||||||
|
+ Tambah
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Submit Button */}
|
||||||
|
<div className="flex justify-end space-x-4">
|
||||||
|
<Button type="button" variant="outline" onClick={() => router.back()}>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={loading}>
|
||||||
|
{loading
|
||||||
|
? "Menyimpan..."
|
||||||
|
: mode === "create"
|
||||||
|
? "Buat User"
|
||||||
|
: "Perbarui User"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,355 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { ChevronLeft, ChevronRight, Search } from "lucide-react";
|
||||||
|
import Navbar from "@/components/navbar";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { RadioGroup } from "@radix-ui/react-radio-group";
|
||||||
|
import { RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
|
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 { createMasterUser } from "@/service/master-user";
|
||||||
|
import { close, error, loading } from "@/config/swal";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
|
const formSchema = z
|
||||||
|
.object({
|
||||||
|
fullname: z.string().min(2, {
|
||||||
|
message: "Nama Lengkap wajib diisi",
|
||||||
|
}),
|
||||||
|
phone: z.string().min(2, {
|
||||||
|
message: "No. Handphone/Whatsapp wajib diisi",
|
||||||
|
}),
|
||||||
|
email: z.string().email({
|
||||||
|
message: "Alamat email wajib valid",
|
||||||
|
}),
|
||||||
|
password: z.string().min(8, {
|
||||||
|
message: "Kata Sandi minimal 8 karakter",
|
||||||
|
}),
|
||||||
|
passwordConfirm: z.string().min(8, {
|
||||||
|
message: "Konfirmasi Kata Sandi minimal 8 karakter",
|
||||||
|
}),
|
||||||
|
degree: z.string().optional(),
|
||||||
|
})
|
||||||
|
.refine((data) => data.password === data.passwordConfirm, {
|
||||||
|
message: "Konfirmasi kata sandi tidak sama",
|
||||||
|
path: ["passwordConfirm"],
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormData = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
export default function CreateUserForm() {
|
||||||
|
const MySwal = withReactContent(Swal);
|
||||||
|
const router = useRouter();
|
||||||
|
const [userType, setUserType] = useState("expert");
|
||||||
|
const [isVisible, setIsVisible] = useState([false, false]);
|
||||||
|
const [lastJob, setLastJob] = useState("");
|
||||||
|
const [lastEducation, setLastEducation] = useState("");
|
||||||
|
const [roleValidation, setRoleValidation] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm<FormData>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
fullname: "",
|
||||||
|
phone: "",
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
passwordConfirm: "",
|
||||||
|
degree: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (data: FormData) => {
|
||||||
|
const validation: string[] = [];
|
||||||
|
|
||||||
|
// if (userType === "expert") {
|
||||||
|
// if (!lastEducation) validation.push("lastEducation");
|
||||||
|
// if (!lastJob) validation.push("lastJob");
|
||||||
|
// }
|
||||||
|
|
||||||
|
// setRoleValidation(validation);
|
||||||
|
|
||||||
|
if (validation.length === 0) {
|
||||||
|
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) => {
|
||||||
|
loading();
|
||||||
|
const request =
|
||||||
|
userType == "expert"
|
||||||
|
? {
|
||||||
|
fullname: data.fullname,
|
||||||
|
userName: data.fullname.toLocaleLowerCase(),
|
||||||
|
password: data.password,
|
||||||
|
userRoleId: 1,
|
||||||
|
userLevelId: 1,
|
||||||
|
email: data.email,
|
||||||
|
whatsappNumber: data.phone,
|
||||||
|
degree: data.degree,
|
||||||
|
identityGroup: "Tenaga Ahli",
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
fullname: data.fullname,
|
||||||
|
userName: data.fullname.toLocaleLowerCase(),
|
||||||
|
password: data.password,
|
||||||
|
userRoleId: 1,
|
||||||
|
userLevelId: 1,
|
||||||
|
email: data.email,
|
||||||
|
whatsappNumber: data.phone,
|
||||||
|
identityGroup: "Pengguna Umum",
|
||||||
|
};
|
||||||
|
const res = await createMasterUser(request);
|
||||||
|
if (res?.error) {
|
||||||
|
error(res?.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
close();
|
||||||
|
successSubmit("/expert-approver/users-management");
|
||||||
|
};
|
||||||
|
|
||||||
|
function successSubmit(redirect: string) {
|
||||||
|
MySwal.fire({
|
||||||
|
title: "Sukses",
|
||||||
|
icon: "success",
|
||||||
|
confirmButtonColor: "#3085d6",
|
||||||
|
confirmButtonText: "OK",
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
router.push(redirect);
|
||||||
|
} else {
|
||||||
|
router.push(redirect);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Navbar
|
||||||
|
title="Managemen Users,/expert-approver/users-management"
|
||||||
|
subTitle="New"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-3 mt-5">
|
||||||
|
<p>Jenis Pengguna</p>
|
||||||
|
<RadioGroup
|
||||||
|
orientation="horizontal"
|
||||||
|
value={userType}
|
||||||
|
onValueChange={setUserType}
|
||||||
|
className="flex flex-row gap-3"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<RadioGroupItem value="expert" id="r1" />
|
||||||
|
<Label htmlFor="r1" className="font-normal">
|
||||||
|
Tenaga Ahli
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<RadioGroupItem value="general" id="r2" />
|
||||||
|
<Label htmlFor="r2" className="font-normal">
|
||||||
|
Pengguna Umum
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
<form
|
||||||
|
className="flex flex-col gap-3 mt-5"
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
>
|
||||||
|
<p>Profile</p>
|
||||||
|
<div className="flex flex-row gap-5">
|
||||||
|
<div
|
||||||
|
className={`flex flex-col gap-1 ${
|
||||||
|
userType == "expert" ? "w-1/2" : "w-full"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="fullname"
|
||||||
|
render={({ field }) => (
|
||||||
|
<div className="relative">
|
||||||
|
<label
|
||||||
|
htmlFor="fullname"
|
||||||
|
className="absolute -top-2 left-3 bg-white px-1 text-xs text-muted-foreground"
|
||||||
|
>
|
||||||
|
Nama Lengkap
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
id="fullname"
|
||||||
|
placeholder="Masukkan Nama Lengkap"
|
||||||
|
className="h-12 rounded-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{errors.fullname && (
|
||||||
|
<p className="text-red-500 text-sm">{errors.fullname.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{userType == "expert" && (
|
||||||
|
<div className="flex flex-col w-1/2 gap-1">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="degree"
|
||||||
|
render={({ field }) => (
|
||||||
|
<div className="relative">
|
||||||
|
<label
|
||||||
|
htmlFor="degree"
|
||||||
|
className="absolute -top-2 left-3 bg-white px-1 text-xs text-muted-foreground"
|
||||||
|
>
|
||||||
|
Gelar
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
id="degree"
|
||||||
|
placeholder="Masukkan Gelar"
|
||||||
|
className="h-12 rounded-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row gap-5">
|
||||||
|
<div className={`flex flex-col gap-1 w-1/2`}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="email"
|
||||||
|
render={({ field }) => (
|
||||||
|
<div className="relative">
|
||||||
|
<label
|
||||||
|
htmlFor="email"
|
||||||
|
className="absolute -top-2 left-3 bg-white px-1 text-xs text-muted-foreground"
|
||||||
|
>
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
placeholder="Masukkan Email"
|
||||||
|
className="h-12 rounded-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{errors.email && (
|
||||||
|
<p className="text-red-500 text-sm">{errors.email.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col w-1/2 gap-1">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="phone"
|
||||||
|
render={({ field }) => (
|
||||||
|
<div className="relative">
|
||||||
|
<label
|
||||||
|
htmlFor="phone"
|
||||||
|
className="absolute -top-2 left-3 bg-white px-1 text-xs text-muted-foreground"
|
||||||
|
>
|
||||||
|
No Whatsapp
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
id="phone"
|
||||||
|
type="number"
|
||||||
|
placeholder="Masukkan No Whatsapp"
|
||||||
|
className="h-12 rounded-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{errors.phone && (
|
||||||
|
<p className="text-red-500 text-sm">{errors.phone.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row gap-5">
|
||||||
|
<div className={`flex flex-col gap-1 w-1/2`}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="password"
|
||||||
|
render={({ field }) => (
|
||||||
|
<div className="relative">
|
||||||
|
<label
|
||||||
|
htmlFor="password"
|
||||||
|
className="absolute -top-2 left-3 bg-white px-1 text-xs text-muted-foreground"
|
||||||
|
>
|
||||||
|
Kata Sandi
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
placeholder="Masukkan Kata Sandi"
|
||||||
|
className="h-12 rounded-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{errors.password && (
|
||||||
|
<p className="text-red-500 text-sm">{errors.password.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col w-1/2 gap-1">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="passwordConfirm"
|
||||||
|
render={({ field }) => (
|
||||||
|
<div className="relative">
|
||||||
|
<label
|
||||||
|
htmlFor="passwordConfirm"
|
||||||
|
className="absolute -top-2 left-3 bg-white px-1 text-xs text-muted-foreground"
|
||||||
|
>
|
||||||
|
Konfirmasi Kata Sandi
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="password"
|
||||||
|
id="passwordConfirm"
|
||||||
|
placeholder="Masukkan Kata Sandi"
|
||||||
|
className="h-12 rounded-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{errors.passwordConfirm && (
|
||||||
|
<p className="text-red-500 text-sm">
|
||||||
|
{errors.passwordConfirm.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button type="submit" className="w-fit bg-sky-600">
|
||||||
|
Simpan Data User
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,252 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { ChevronLeft, ChevronRight, Search } from "lucide-react";
|
||||||
|
import Navbar from "@/components/navbar";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { RadioGroup } from "@radix-ui/react-radio-group";
|
||||||
|
import { RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
|
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 { editMasterUsers, getDetailMasterUsers } from "@/service/master-user";
|
||||||
|
import { close, error, loading } from "@/config/swal";
|
||||||
|
import { useParams, useRouter } from "next/navigation";
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
fullname: z.string().min(2, {
|
||||||
|
message: "Nama Lengkap wajib diisi",
|
||||||
|
}),
|
||||||
|
phone: z.string().min(2, {
|
||||||
|
message: "No. Handphone/Whatsapp wajib diisi",
|
||||||
|
}),
|
||||||
|
email: z.string().email({
|
||||||
|
message: "Alamat email wajib valid",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormData = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
interface UserData {
|
||||||
|
id: number;
|
||||||
|
fullname: string;
|
||||||
|
email: string;
|
||||||
|
whatsappNumber: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DetailUserForm() {
|
||||||
|
const MySwal = withReactContent(Swal);
|
||||||
|
const router = useRouter();
|
||||||
|
const params = useParams();
|
||||||
|
const id = params?.id;
|
||||||
|
const [isVisible, setIsVisible] = useState([false, false]);
|
||||||
|
const [lastJob, setLastJob] = useState("");
|
||||||
|
const [lastEducation, setLastEducation] = useState("");
|
||||||
|
const [roleValidation, setRoleValidation] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const [detailUser, setDetailUser] = useState<UserData>();
|
||||||
|
const [isEdit, setIsEdit] = useState(false);
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
setValue,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm<FormData>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
fullname: "",
|
||||||
|
phone: "",
|
||||||
|
email: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchDataUser();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchDataUser = async () => {
|
||||||
|
loading();
|
||||||
|
const res = await getDetailMasterUsers(id as string);
|
||||||
|
const data = res?.data?.data;
|
||||||
|
setDetailUser(data);
|
||||||
|
setValue("fullname", data?.fullname);
|
||||||
|
setValue("phone", data?.whatsappNumber);
|
||||||
|
setValue("email", data?.email);
|
||||||
|
close();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async (data: FormData) => {
|
||||||
|
const validation: string[] = [];
|
||||||
|
|
||||||
|
setRoleValidation(validation);
|
||||||
|
|
||||||
|
if (validation.length === 0) {
|
||||||
|
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) => {
|
||||||
|
loading();
|
||||||
|
const request = {
|
||||||
|
fullname: data.fullname,
|
||||||
|
userName: data.fullname.toLocaleLowerCase(),
|
||||||
|
email: data.email,
|
||||||
|
whatsappNumber: data.phone,
|
||||||
|
userRoleId: 1,
|
||||||
|
userLevelId: 1,
|
||||||
|
};
|
||||||
|
const res = await editMasterUsers(request, id as string);
|
||||||
|
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) {
|
||||||
|
fetchDataUser();
|
||||||
|
} else {
|
||||||
|
fetchDataUser();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Navbar
|
||||||
|
title="Managemen Users,/expert-approver/users-management"
|
||||||
|
subTitle="Detail"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex justify-between gap-3 mt-8">
|
||||||
|
<div className="flex flex-row gap-2">
|
||||||
|
<p>{detailUser?.fullname}</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={() => setIsEdit(true)}
|
||||||
|
className={`cursor-pointer ${isEdit ? "bg-green-500" : ""}`}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
<Button className="bg-sky-600 cursor-pointer">Aktivasi</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form
|
||||||
|
className="flex flex-col gap-3 mt-5"
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
>
|
||||||
|
<p>Profile</p>
|
||||||
|
<div className={`flex flex-col gap-1 w-1/2`}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="fullname"
|
||||||
|
render={({ field }) => (
|
||||||
|
<div className="relative">
|
||||||
|
<label
|
||||||
|
htmlFor="fullname"
|
||||||
|
className="absolute -top-2 left-3 bg-white px-1 text-xs text-muted-foreground"
|
||||||
|
>
|
||||||
|
Nama Lengkap
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
id="fullname"
|
||||||
|
readOnly={!isEdit}
|
||||||
|
placeholder="Masukkan Nama Lengkap"
|
||||||
|
className="h-12 rounded-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{errors.fullname && (
|
||||||
|
<p className="text-red-500 text-sm">{errors.fullname.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={`flex flex-col gap-1 w-1/2`}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="email"
|
||||||
|
render={({ field }) => (
|
||||||
|
<div className="relative">
|
||||||
|
<label
|
||||||
|
htmlFor="email"
|
||||||
|
className="absolute -top-2 left-3 bg-white px-1 text-xs text-muted-foreground"
|
||||||
|
>
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
id="email"
|
||||||
|
readOnly={!isEdit}
|
||||||
|
type="email"
|
||||||
|
placeholder="Masukkan Email"
|
||||||
|
className="h-12 rounded-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{errors.email && (
|
||||||
|
<p className="text-red-500 text-sm">{errors.email.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col w-1/2 gap-1">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="phone"
|
||||||
|
render={({ field }) => (
|
||||||
|
<div className="relative">
|
||||||
|
<label
|
||||||
|
htmlFor="phone"
|
||||||
|
className="absolute -top-2 left-3 bg-white px-1 text-xs text-muted-foreground"
|
||||||
|
>
|
||||||
|
No Whatsapp
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
id="phone"
|
||||||
|
readOnly={!isEdit}
|
||||||
|
type="number"
|
||||||
|
placeholder="Masukkan No Whatsapp"
|
||||||
|
className="h-12 rounded-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{errors.phone && (
|
||||||
|
<p className="text-red-500 text-sm">{errors.phone.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button type="submit" className="w-fit bg-sky-600">
|
||||||
|
Simpan Data User
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
import { Search } from "lucide-react";
|
||||||
|
import { Input } from "./ui/input";
|
||||||
|
import { Button } from "./ui/button";
|
||||||
|
import { useState } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export default function Navbar(props: {
|
||||||
|
title: string;
|
||||||
|
subTitle?: string;
|
||||||
|
subSubTitle?: string;
|
||||||
|
}) {
|
||||||
|
const { title, subTitle, subSubTitle } = props;
|
||||||
|
const titleCondition = subSubTitle
|
||||||
|
? "subSubTitle"
|
||||||
|
: subTitle
|
||||||
|
? "subTitle"
|
||||||
|
: "title";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full flex justify-between items-center h-8">
|
||||||
|
<div className="flex flex-row">
|
||||||
|
<Link href={titleCondition == "title" ? "" : title.split(",")[1]}>
|
||||||
|
<h1
|
||||||
|
className={`text-md ${
|
||||||
|
titleCondition == "title" ? "font-semibold" : "font-light"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{title.split(",")[0]}
|
||||||
|
</h1>
|
||||||
|
</Link>
|
||||||
|
{subTitle && <p className="mx-3">-</p>}
|
||||||
|
{subTitle && (
|
||||||
|
<Link
|
||||||
|
href={titleCondition == "subTitle" ? "" : subTitle.split(",")[1]}
|
||||||
|
>
|
||||||
|
<h1
|
||||||
|
className={`text-md ${
|
||||||
|
titleCondition == "subTitle" ? "font-semibold" : "font-light"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{subTitle.split(",")[0]}
|
||||||
|
</h1>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
{subSubTitle && <p className="mx-3">-</p>}
|
||||||
|
{subSubTitle && (
|
||||||
|
<Link
|
||||||
|
href={
|
||||||
|
titleCondition == "subSubTitle"
|
||||||
|
? "font-light"
|
||||||
|
: subSubTitle.split(",")[1]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
s
|
||||||
|
<h1
|
||||||
|
className={`text-md ${
|
||||||
|
titleCondition == "subSubTitle" ? "font-semibold" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{subSubTitle.split(",")[0]}
|
||||||
|
</h1>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row gap-3">
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-2 top-2.5 h-4 w-4 text-gray-500" />
|
||||||
|
<Input placeholder="Search" className="pl-8 text-sm h-8 w-48" />
|
||||||
|
</div>
|
||||||
|
<Button className="h-8">Notif</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -46,7 +46,8 @@ export default function Sidebar() {
|
||||||
const uid = Cookies.get("uid");
|
const uid = Cookies.get("uid");
|
||||||
try {
|
try {
|
||||||
const result = await getChatHistory(uid ?? "user_123");
|
const result = await getChatHistory(uid ?? "user_123");
|
||||||
setSessions(result.sessions);
|
console.log("data", result.data);
|
||||||
|
setSessions(result.data || []);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching sessions:", error);
|
console.error("Error fetching sessions:", error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -68,7 +69,7 @@ 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);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
|
|
@ -340,11 +341,117 @@ export default function Sidebar() {
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href={"/admin/dashboard"}>
|
<Link href={"/admin/dashboard"}>
|
||||||
<button className="flex items-center w-full gap-2 px-3 py-2 rounded hover:bg-gray-200 cursor-pointer mb-2">
|
<button className="flex items-center w-full gap-2 px-3 py-2 rounded hover:bg-gray-200 cursor-pointer mb-2 ml-1">
|
||||||
{!isCollapsed && <Plus size={16} />}
|
{!isCollapsed && <Plus size={16} />}
|
||||||
{!isCollapsed && <span>New Chat</span>}
|
{!isCollapsed && <span>New Chat</span>}
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
|
<details className="group mb-2 ml-1">
|
||||||
|
<summary className="flex items-center gap-2 cursor-pointer px-2 py-2 rounded hover:bg-gray-100">
|
||||||
|
{!isCollapsed && (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M13 3a9 9 0 0 0-9 9H1l3.89 3.89l.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7s-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42A8.95 8.95 0 0 0 13 21a9 9 0 0 0 0-18m-1 5v5l4.28 2.54l.72-1.21l-3.5-2.08V8z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
{!isCollapsed && <span>History</span>}
|
||||||
|
{!isCollapsed && (
|
||||||
|
<div className="ml-auto flex items-center gap-2">
|
||||||
|
{sessions.length > 0 && (
|
||||||
|
<span className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
|
||||||
|
{sessions.length}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<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">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Link
|
||||||
|
href="/admin/history"
|
||||||
|
className="hover:text-gray-700 transition-colors font-medium"
|
||||||
|
>
|
||||||
|
Semua Riwayat ({sessions.length})
|
||||||
|
</Link>
|
||||||
|
<button
|
||||||
|
onClick={fetchSessions}
|
||||||
|
disabled={loading}
|
||||||
|
className="p-1 hover:bg-gray-200 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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{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 hover:bg-gray-100 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="text-sm font-medium text-gray-700 line-clamp-1">
|
||||||
|
{session.title}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-xs text-gray-400 mt-1">
|
||||||
|
{/* <span>{session.message_count} pesan</span> */}
|
||||||
|
{/* <span>•</span> */}
|
||||||
|
<span>
|
||||||
|
{formatRelativeTime(session.created_at)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-xs text-gray-400">
|
||||||
|
Belum ada riwayat chat
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</details>
|
||||||
<Link href={"/admin/live-consultation"}>
|
<Link href={"/admin/live-consultation"}>
|
||||||
<button className="flex items-center w-full gap-2 px-3 py-2 rounded hover:bg-gray-200 cursor-pointer mb-2">
|
<button className="flex items-center w-full gap-2 px-3 py-2 rounded hover:bg-gray-200 cursor-pointer mb-2">
|
||||||
{!isCollapsed && (
|
{!isCollapsed && (
|
||||||
|
|
@ -554,6 +661,25 @@ export default function Sidebar() {
|
||||||
)}
|
)}
|
||||||
{roleId === "1" && (
|
{roleId === "1" && (
|
||||||
<>
|
<>
|
||||||
|
<Link href={"/admin/management-user"}>
|
||||||
|
<button className="flex items-center w-full gap-2 px-3 py-2 rounded hover:bg-gray-100 cursor-pointer mb-2">
|
||||||
|
{!isCollapsed && (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="6" r="4" fill="currentColor" />
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M20 17.5c0 2.485 0 4.5-8 4.5s-8-2.015-8-4.5S7.582 13 12 13s8 2.015 8 4.5"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
{!isCollapsed && <span>Manajemen User</span>}
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
<Link href={"/admin/management-ebook"}>
|
<Link href={"/admin/management-ebook"}>
|
||||||
<button className="flex items-center w-full gap-2 px-3 py-2 rounded hover:bg-gray-100 cursor-pointer mb-2">
|
<button className="flex items-center w-full gap-2 px-3 py-2 rounded hover:bg-gray-100 cursor-pointer mb-2">
|
||||||
{!isCollapsed && (
|
{!isCollapsed && (
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,160 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { ChevronLeft, ChevronRight, Search } from "lucide-react";
|
||||||
|
import Navbar from "@/components/navbar";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { listMasterUsers } from "@/service/master-user";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCaption,
|
||||||
|
TableCell,
|
||||||
|
TableFooter,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import CustomPagination from "@/components/custom-pagination";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { close, loading } from "@/config/swal";
|
||||||
|
|
||||||
|
interface TableData {
|
||||||
|
id: number;
|
||||||
|
fullname: string;
|
||||||
|
email: string;
|
||||||
|
userLevelGroup: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UsersManagementTable() {
|
||||||
|
const [usersDataTable, setUsersDataTable] = useState<TableData[]>([]);
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [limit, setLimit] = useState("10");
|
||||||
|
const [summaryData, setSummaryData] = useState({
|
||||||
|
totalData: 1,
|
||||||
|
totalPage: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchDataTable();
|
||||||
|
}, [page]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setPage(1);
|
||||||
|
fetchDataTable();
|
||||||
|
}, [limit]);
|
||||||
|
|
||||||
|
const fetchDataTable = async () => {
|
||||||
|
loading();
|
||||||
|
const request = { page: page, limit: limit };
|
||||||
|
const res = await listMasterUsers(request);
|
||||||
|
setUsersDataTable(res?.data?.data?.length > 0 ? res?.data?.data : []);
|
||||||
|
setSummaryData({
|
||||||
|
totalData: res?.data?.meta?.count,
|
||||||
|
totalPage: res?.data?.meta?.totalPage,
|
||||||
|
});
|
||||||
|
close();
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Navbar title="Managemen Users" />
|
||||||
|
<div className="flex flex-row gap-3 mt-5 items-center">
|
||||||
|
<div className="relative w-1/6">
|
||||||
|
<label
|
||||||
|
htmlFor="search"
|
||||||
|
className="absolute -top-2 left-3 bg-white px-1 text-xs text-muted-foreground"
|
||||||
|
>
|
||||||
|
Search
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
id="search"
|
||||||
|
placeholder="Name, email, etc..."
|
||||||
|
className="h-12 rounded-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Link href="/expert-approver/users-management/create">
|
||||||
|
<Button className="bg-sky-600">NEW</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Table className="border-2 mt-5">
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Nama Lengkap</TableHead>
|
||||||
|
<TableHead>Email</TableHead>
|
||||||
|
<TableHead>Jenis Akun</TableHead>
|
||||||
|
<TableHead>Tanggal Daftar</TableHead>
|
||||||
|
<TableHead>Status</TableHead>
|
||||||
|
<TableHead>Tindakan</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{usersDataTable.map((user) => (
|
||||||
|
<TableRow key={user.id}>
|
||||||
|
<TableCell>{user.fullname}</TableCell>
|
||||||
|
<TableCell>{user.email}</TableCell>
|
||||||
|
<TableCell>{user.userLevelGroup.toLocaleUpperCase()}</TableCell>
|
||||||
|
<TableCell>{user.createdAt}</TableCell>
|
||||||
|
<TableCell>Aktif</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex flex-row gap-1">
|
||||||
|
<Link
|
||||||
|
href={`/expert-approver/users-management/detail/${user.id}`}
|
||||||
|
className="text-blue-600 cursor-pointer"
|
||||||
|
>
|
||||||
|
Lihat
|
||||||
|
</Link>
|
||||||
|
<a className="text-red-600 cursor-pointer">Hapus</a>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
<TableFooter>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={6}>
|
||||||
|
<div className="flex flex-row items-center justify-end w-full">
|
||||||
|
<p className="text-sm font-light">Rows per page: </p>
|
||||||
|
<Select value={limit} onValueChange={setLimit}>
|
||||||
|
<SelectTrigger className="!border-none">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectItem value="5">5</SelectItem>
|
||||||
|
<SelectItem value="10">10</SelectItem>
|
||||||
|
<SelectItem value="25">25</SelectItem>
|
||||||
|
<SelectItem value="50">50</SelectItem>
|
||||||
|
<SelectItem value="100">100</SelectItem>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-sm font-light">
|
||||||
|
{Number(limit) * (page - 1) + 1} -{" "}
|
||||||
|
{Number(limit) * (page - 1) + usersDataTable.length} of{" "}
|
||||||
|
{summaryData.totalData}
|
||||||
|
</p>
|
||||||
|
<CustomPagination
|
||||||
|
totalPage={summaryData.totalPage}
|
||||||
|
onPageChange={setPage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableFooter>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
|
|
@ -20,73 +20,73 @@ import {
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { BellIcon } from "lucide-react";
|
import { BellIcon, Plus } from "lucide-react";
|
||||||
|
import { getAllEbooks } from "@/service/ebook";
|
||||||
|
import { toast } from "react-hot-toast";
|
||||||
|
|
||||||
type Ebook = {
|
type Ebook = {
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
author: string;
|
authorName: string;
|
||||||
date: string;
|
createdAt: string;
|
||||||
price: string;
|
price: number;
|
||||||
status: "Published" | "Draft";
|
isPublished: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const data: Ebook[] = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
title: "Komunikasi Strategis di Era Digital",
|
|
||||||
author: "Prof. Dr. Haryanto Wijaya, S.H., LL.M.",
|
|
||||||
date: "14 Januari 2025 13:00:00",
|
|
||||||
price: "Rp 50.000",
|
|
||||||
status: "Published",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
title: "Transformasi Digital & Inovasi Teknologi Informasi",
|
|
||||||
author: "Ir. Agus Gusti Utama, M.Sc., Ph.D.",
|
|
||||||
date: "14 Januari 2025 13:00:00",
|
|
||||||
price: "Rp 100.000",
|
|
||||||
status: "Published",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
title: "Bahasa, Identitas, dan Kekuasaan",
|
|
||||||
author: "Mira Anggraini, S.S., M.Pd.",
|
|
||||||
date: "14 Januari 2025 13:00:00",
|
|
||||||
price: "Rp 100.000",
|
|
||||||
status: "Published",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
title: "Politik 5.0: Demokrasi, Teknologi, dan Masyarakat Masa Depan",
|
|
||||||
author: "Prof. Miftahul Arifin, M.A., Ph.D.",
|
|
||||||
date: "14 Januari 2025 13:00:00",
|
|
||||||
price: "Rp 100.000",
|
|
||||||
status: "Published",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
title: "Manajemen Komunikasi Lintas Budaya",
|
|
||||||
author: "Prof. Dr. Haryanto Wijaya, S.H., LL.M.",
|
|
||||||
date: "14 Januari 2025 13:00:00",
|
|
||||||
price: "Rp 100.000",
|
|
||||||
status: "Published",
|
|
||||||
},
|
|
||||||
// Add more as needed
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function ManajemenEbook() {
|
export default function ManajemenEbook() {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [statusFilter, setStatusFilter] = useState("Semua");
|
const [statusFilter, setStatusFilter] = useState("Semua");
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
|
const [data, setData] = useState<Ebook[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
const rowsPerPage = 5;
|
const rowsPerPage = 5;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadEbooks();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadEbooks = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await getAllEbooks();
|
||||||
|
if (!response.error && response.data?.data) {
|
||||||
|
setData(response.data.data);
|
||||||
|
} else {
|
||||||
|
toast.error("Gagal memuat data ebook");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Terjadi kesalahan saat memuat data ebook");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatCurrency = (amount: number) => {
|
||||||
|
return new Intl.NumberFormat('id-ID', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'IDR',
|
||||||
|
minimumFractionDigits: 0,
|
||||||
|
}).format(amount);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateString: string) => {
|
||||||
|
return new Date(dateString).toLocaleDateString('id-ID', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const filtered = data.filter((ebook) => {
|
const filtered = data.filter((ebook) => {
|
||||||
const matchesSearch = ebook.title
|
const matchesSearch = ebook.title
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.includes(search.toLowerCase());
|
.includes(search.toLowerCase());
|
||||||
const matchesStatus =
|
const matchesStatus =
|
||||||
statusFilter === "Semua" || ebook.status === statusFilter;
|
statusFilter === "Semua" ||
|
||||||
|
(statusFilter === "Published" && ebook.isPublished) ||
|
||||||
|
(statusFilter === "Draft" && !ebook.isPublished);
|
||||||
return matchesSearch && matchesStatus;
|
return matchesSearch && matchesStatus;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -96,11 +96,25 @@ export default function ManajemenEbook() {
|
||||||
);
|
);
|
||||||
const pageCount = Math.ceil(filtered.length / rowsPerPage);
|
const pageCount = Math.ceil(filtered.length / rowsPerPage);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center h-64">
|
||||||
|
<div className="text-lg">Memuat data ebook...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h1 className="text-lg font-semibold">Manajemen E-Book</h1>
|
<h1 className="text-lg font-semibold">Manajemen E-Book</h1>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
|
<Link href="/admin/management-ebook/create">
|
||||||
|
<Button className="flex items-center space-x-2">
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
<span>Tambah Ebook</span>
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
value={search}
|
value={search}
|
||||||
|
|
@ -153,28 +167,36 @@ export default function ManajemenEbook() {
|
||||||
{paginated.map((ebook) => (
|
{paginated.map((ebook) => (
|
||||||
<TableRow key={ebook.id}>
|
<TableRow key={ebook.id}>
|
||||||
<TableCell>{ebook.title}</TableCell>
|
<TableCell>{ebook.title}</TableCell>
|
||||||
<TableCell>{ebook.author}</TableCell>
|
<TableCell>{ebook.authorName}</TableCell>
|
||||||
<TableCell>{ebook.date}</TableCell>
|
<TableCell>{formatDate(ebook.createdAt)}</TableCell>
|
||||||
<TableCell>{ebook.price}</TableCell>
|
<TableCell>{formatCurrency(ebook.price)}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"px-2 py-1 rounded text-white text-xs",
|
"px-2 py-1 rounded text-white text-xs",
|
||||||
ebook.status === "Published"
|
ebook.isPublished
|
||||||
? "bg-green-500"
|
? "bg-green-500"
|
||||||
: "bg-yellow-500"
|
: "bg-yellow-500"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{ebook.status}
|
{ebook.isPublished ? "Published" : "Draft"}
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Link
|
<div className="flex space-x-2">
|
||||||
className="text-blue-600 px-0"
|
<Link
|
||||||
href={`/admin/management-ebook/detail/${ebook?.id}`}
|
className="text-blue-600 px-0"
|
||||||
>
|
href={`/admin/management-ebook/detail/${ebook?.id}`}
|
||||||
<Button variant="link">Lihat</Button>
|
>
|
||||||
</Link>
|
<Button variant="link" size="sm">Lihat</Button>
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
className="text-green-600 px-0"
|
||||||
|
href={`/admin/management-ebook/edit/${ebook?.id}`}
|
||||||
|
>
|
||||||
|
<Button variant="link" size="sm">Edit</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -1,63 +1,123 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Input } from "../ui/input";
|
import { Input } from "../ui/input";
|
||||||
import { BellIcon } from "lucide-react";
|
import { Button } from "../ui/button";
|
||||||
|
import { BellIcon, Plus } from "lucide-react";
|
||||||
|
import { getAllUsers } from "@/service/user";
|
||||||
|
import { toast } from "react-hot-toast";
|
||||||
|
import { error, success } from "@/config/swal";
|
||||||
|
import withReactContent from "sweetalert2-react-content";
|
||||||
|
import Swal from "sweetalert2";
|
||||||
|
import { deleteMasterUser } from "@/service/master-user";
|
||||||
|
|
||||||
const usersData = [
|
type User = {
|
||||||
{
|
id: number;
|
||||||
id: 1,
|
fullname: string;
|
||||||
name: "Novan Farhandi",
|
email: string;
|
||||||
email: "novanfarhandi@example.com",
|
user_role_id: number;
|
||||||
role: "Pengguna Umum",
|
created_at: string;
|
||||||
date: "14 Januari 2025 13:00:00",
|
is_active: boolean;
|
||||||
active: true,
|
};
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: "Prof. Dr. Haryanto Wijaya, S.H., LL.M.",
|
|
||||||
email: "haryanto@example.com",
|
|
||||||
role: "Tenaga Ahli",
|
|
||||||
date: "14 Januari 2025 13:00:00",
|
|
||||||
active: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: "Prof. Dr. Farhan",
|
|
||||||
email: "farhan@example.com",
|
|
||||||
role: "Tenaga Ahli",
|
|
||||||
date: "14 Januari 2025 13:00:00",
|
|
||||||
active: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
name: "Salma Husna",
|
|
||||||
email: "salmahusna@example.com",
|
|
||||||
role: "Pengguna Umum",
|
|
||||||
date: "14 Januari 2025 13:00:00",
|
|
||||||
active: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
name: "Prof. Dr. Sheva",
|
|
||||||
email: "sheva@example.com",
|
|
||||||
role: "Tenaga Ahli",
|
|
||||||
date: "14 Januari 2025 13:00:00",
|
|
||||||
active: false,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function ManajemenUser() {
|
export default function ManajemenUser() {
|
||||||
const [users, setUsers] = useState(usersData);
|
const [mounted, setMounted] = useState(false);
|
||||||
|
const [users, setUsers] = useState<User[]>([]);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const MySwal = withReactContent(Swal);
|
||||||
|
|
||||||
const toggleActive = (id: number) => {
|
useEffect(() => {
|
||||||
setUsers((prev) =>
|
setMounted(true);
|
||||||
prev.map((user) =>
|
loadUsers();
|
||||||
user.id === id ? { ...user, active: !user.active } : user
|
}, []);
|
||||||
)
|
|
||||||
|
const loadUsers = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await getAllUsers();
|
||||||
|
if (!response.error && response.data?.data) {
|
||||||
|
setUsers(response.data.data);
|
||||||
|
} else {
|
||||||
|
toast.error("Gagal memuat data users");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Terjadi kesalahan saat memuat data users");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUserRoleName = (roleId: number) => {
|
||||||
|
switch (roleId) {
|
||||||
|
case 2:
|
||||||
|
return "Tenaga Ahli";
|
||||||
|
case 3:
|
||||||
|
return "Pengguna Umum";
|
||||||
|
default:
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateString: string) => {
|
||||||
|
return new Date(dateString).toLocaleDateString("id-ID", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredUsers = users.filter(
|
||||||
|
(user) =>
|
||||||
|
user.fullname.toLowerCase().includes(search.toLowerCase()) ||
|
||||||
|
user.email.toLowerCase().includes(search.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center h-64">
|
||||||
|
<div className="text-lg">Loading...</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center h-64">
|
||||||
|
<div className="text-lg">Memuat data users...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doDelete(id: any) {
|
||||||
|
// loading();
|
||||||
|
const resDelete = await deleteMasterUser(id);
|
||||||
|
|
||||||
|
if (resDelete?.error) {
|
||||||
|
error(resDelete.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
close();
|
||||||
|
success("Berhasil Hapus");
|
||||||
|
loadUsers();
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = (id: any) => {
|
||||||
|
MySwal.fire({
|
||||||
|
title: "Hapus Data",
|
||||||
|
icon: "warning",
|
||||||
|
showCancelButton: true,
|
||||||
|
cancelButtonColor: "#3085d6",
|
||||||
|
confirmButtonColor: "#d33",
|
||||||
|
confirmButtonText: "Hapus",
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
doDelete(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -65,6 +125,12 @@ export default function ManajemenUser() {
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h1 className="text-lg font-semibold">Manajemen Users</h1>
|
<h1 className="text-lg font-semibold">Manajemen Users</h1>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
|
<Link href="/admin/management-user/create">
|
||||||
|
<Button className="flex items-center space-x-2">
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
<span>Tambah User</span>
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
value={search}
|
value={search}
|
||||||
|
|
@ -79,25 +145,20 @@ export default function ManajemenUser() {
|
||||||
</div>
|
</div>
|
||||||
{/* Filter Section */}
|
{/* 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
|
||||||
type="text"
|
|
||||||
placeholder="Name, email, etc..."
|
placeholder="Name, email, etc..."
|
||||||
className="border rounded px-3 py-2 text-sm w-56"
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
className="w-56"
|
||||||
/>
|
/>
|
||||||
<input type="date" className="border rounded px-3 py-2 text-sm" />
|
<Input type="date" className="w-40" />
|
||||||
<span>-</span>
|
<span>-</span>
|
||||||
<input type="date" className="border rounded px-3 py-2 text-sm" />
|
<Input type="date" className="w-40" />
|
||||||
<select className="border rounded px-3 py-2 text-sm">
|
<select className="border rounded px-3 py-2 text-sm">
|
||||||
<option>Semua</option>
|
<option>Semua</option>
|
||||||
<option>Pengguna Umum</option>
|
<option>Pengguna Umum</option>
|
||||||
<option>Tenaga Ahli</option>
|
<option>Tenaga Ahli</option>
|
||||||
</select>
|
</select>
|
||||||
<Link
|
|
||||||
className="bg-blue-500 text-white px-4 py-2 rounded ml-auto"
|
|
||||||
href={"/admin/management-user/create"}
|
|
||||||
>
|
|
||||||
<button className="bg-blue-500 ">NEW</button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Table */}
|
{/* Table */}
|
||||||
|
|
@ -114,39 +175,57 @@ export default function ManajemenUser() {
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{users.map((u) => (
|
{filteredUsers.map((user) => (
|
||||||
<tr key={u.id} className="border-b">
|
<tr key={user.id} className="border-b">
|
||||||
<td className="px-4 py-2">{u.name}</td>
|
<td className="px-4 py-2">{user.fullname}</td>
|
||||||
<td className="px-4 py-2">{u.email}</td>
|
<td className="px-4 py-2">{user.email}</td>
|
||||||
<td className="px-4 py-2">{u.role}</td>
|
<td className="px-4 py-2">
|
||||||
<td className="px-4 py-2">{u.date}</td>
|
{getUserRoleName(user.user_role_id)}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2">{formatDate(user.created_at)}</td>
|
||||||
<td className="px-4 py-2 flex items-center gap-2">
|
<td className="px-4 py-2 flex items-center gap-2">
|
||||||
<span
|
<span
|
||||||
className={!u.active ? "text-gray-500" : "text-gray-400"}
|
className={
|
||||||
|
!user.is_active ? "text-gray-500" : "text-gray-400"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Tidak Aktif
|
Tidak Aktif
|
||||||
</span>
|
</span>
|
||||||
<label className="relative inline-flex items-center cursor-pointer">
|
<label className="relative inline-flex items-center cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={u.active}
|
checked={user.is_active}
|
||||||
onChange={() => toggleActive(u.id)}
|
|
||||||
className="sr-only peer"
|
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="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>
|
<div className="absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform peer-checked:translate-x-5"></div>
|
||||||
</label>
|
</label>
|
||||||
<span
|
<span
|
||||||
className={u.active ? "text-gray-500" : "text-gray-400"}
|
className={
|
||||||
|
user.is_active ? "text-gray-500" : "text-gray-400"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Aktif
|
Aktif
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2 space-x-3">
|
<td className="px-4 py-2 space-x-3">
|
||||||
<button className="text-blue-500 hover:underline">
|
<Link
|
||||||
|
href={`/admin/management-user/detail/${user.id}`}
|
||||||
|
className="text-blue-500 hover:underline"
|
||||||
|
>
|
||||||
Lihat
|
Lihat
|
||||||
</button>
|
</Link>
|
||||||
<button className="text-red-500 hover:underline">
|
<Link
|
||||||
|
href={`/admin/management-user/edit/${user.id}`}
|
||||||
|
className="text-green-500 hover:underline"
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Link>
|
||||||
|
<button
|
||||||
|
className="text-red-500 hover:underline"
|
||||||
|
onClick={() => handleDelete(user.id)}
|
||||||
|
>
|
||||||
Hapus
|
Hapus
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -165,7 +244,9 @@ export default function ManajemenUser() {
|
||||||
<option>20</option>
|
<option>20</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>1–5 of 13</div>
|
<div>
|
||||||
|
1–{filteredUsers.length} of {filteredUsers.length}
|
||||||
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button className="px-2"><</button>
|
<button className="px-2"><</button>
|
||||||
<button className="px-2">></button>
|
<button className="px-2">></button>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Toaster } from "react-hot-toast";
|
||||||
|
|
||||||
|
export default function ToastProvider() {
|
||||||
|
return (
|
||||||
|
<Toaster
|
||||||
|
position="top-right"
|
||||||
|
toastOptions={{
|
||||||
|
duration: 4000,
|
||||||
|
style: {
|
||||||
|
background: '#363636',
|
||||||
|
color: '#fff',
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
duration: 3000,
|
||||||
|
style: {
|
||||||
|
background: '#10B981',
|
||||||
|
color: '#fff',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
duration: 5000,
|
||||||
|
style: {
|
||||||
|
background: '#EF4444',
|
||||||
|
color: '#fff',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const badgeVariants = cva(
|
||||||
|
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||||
|
secondary:
|
||||||
|
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
destructive:
|
||||||
|
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||||
|
outline: "text-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface BadgeProps
|
||||||
|
extends React.HTMLAttributes<HTMLDivElement>,
|
||||||
|
VariantProps<typeof badgeVariants> {}
|
||||||
|
|
||||||
|
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||||
|
return (
|
||||||
|
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge, badgeVariants }
|
||||||
|
|
@ -0,0 +1,213 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import {
|
||||||
|
ChevronDownIcon,
|
||||||
|
ChevronLeftIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
|
} from "lucide-react"
|
||||||
|
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Button, buttonVariants } from "@/components/ui/button"
|
||||||
|
|
||||||
|
function Calendar({
|
||||||
|
className,
|
||||||
|
classNames,
|
||||||
|
showOutsideDays = true,
|
||||||
|
captionLayout = "label",
|
||||||
|
buttonVariant = "ghost",
|
||||||
|
formatters,
|
||||||
|
components,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DayPicker> & {
|
||||||
|
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
|
||||||
|
}) {
|
||||||
|
const defaultClassNames = getDefaultClassNames()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DayPicker
|
||||||
|
showOutsideDays={showOutsideDays}
|
||||||
|
className={cn(
|
||||||
|
"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
|
||||||
|
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
|
||||||
|
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
captionLayout={captionLayout}
|
||||||
|
formatters={{
|
||||||
|
formatMonthDropdown: (date) =>
|
||||||
|
date.toLocaleString("default", { month: "short" }),
|
||||||
|
...formatters,
|
||||||
|
}}
|
||||||
|
classNames={{
|
||||||
|
root: cn("w-fit", defaultClassNames.root),
|
||||||
|
months: cn(
|
||||||
|
"flex gap-4 flex-col md:flex-row relative",
|
||||||
|
defaultClassNames.months
|
||||||
|
),
|
||||||
|
month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
|
||||||
|
nav: cn(
|
||||||
|
"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
|
||||||
|
defaultClassNames.nav
|
||||||
|
),
|
||||||
|
button_previous: cn(
|
||||||
|
buttonVariants({ variant: buttonVariant }),
|
||||||
|
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
|
||||||
|
defaultClassNames.button_previous
|
||||||
|
),
|
||||||
|
button_next: cn(
|
||||||
|
buttonVariants({ variant: buttonVariant }),
|
||||||
|
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
|
||||||
|
defaultClassNames.button_next
|
||||||
|
),
|
||||||
|
month_caption: cn(
|
||||||
|
"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
|
||||||
|
defaultClassNames.month_caption
|
||||||
|
),
|
||||||
|
dropdowns: cn(
|
||||||
|
"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
|
||||||
|
defaultClassNames.dropdowns
|
||||||
|
),
|
||||||
|
dropdown_root: cn(
|
||||||
|
"relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
|
||||||
|
defaultClassNames.dropdown_root
|
||||||
|
),
|
||||||
|
dropdown: cn(
|
||||||
|
"absolute bg-popover inset-0 opacity-0",
|
||||||
|
defaultClassNames.dropdown
|
||||||
|
),
|
||||||
|
caption_label: cn(
|
||||||
|
"select-none font-medium",
|
||||||
|
captionLayout === "label"
|
||||||
|
? "text-sm"
|
||||||
|
: "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
|
||||||
|
defaultClassNames.caption_label
|
||||||
|
),
|
||||||
|
table: "w-full border-collapse",
|
||||||
|
weekdays: cn("flex", defaultClassNames.weekdays),
|
||||||
|
weekday: cn(
|
||||||
|
"text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
|
||||||
|
defaultClassNames.weekday
|
||||||
|
),
|
||||||
|
week: cn("flex w-full mt-2", defaultClassNames.week),
|
||||||
|
week_number_header: cn(
|
||||||
|
"select-none w-(--cell-size)",
|
||||||
|
defaultClassNames.week_number_header
|
||||||
|
),
|
||||||
|
week_number: cn(
|
||||||
|
"text-[0.8rem] select-none text-muted-foreground",
|
||||||
|
defaultClassNames.week_number
|
||||||
|
),
|
||||||
|
day: cn(
|
||||||
|
"relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
|
||||||
|
defaultClassNames.day
|
||||||
|
),
|
||||||
|
range_start: cn(
|
||||||
|
"rounded-l-md bg-accent",
|
||||||
|
defaultClassNames.range_start
|
||||||
|
),
|
||||||
|
range_middle: cn("rounded-none", defaultClassNames.range_middle),
|
||||||
|
range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
|
||||||
|
today: cn(
|
||||||
|
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
|
||||||
|
defaultClassNames.today
|
||||||
|
),
|
||||||
|
outside: cn(
|
||||||
|
"text-muted-foreground aria-selected:text-muted-foreground",
|
||||||
|
defaultClassNames.outside
|
||||||
|
),
|
||||||
|
disabled: cn(
|
||||||
|
"text-muted-foreground opacity-50",
|
||||||
|
defaultClassNames.disabled
|
||||||
|
),
|
||||||
|
hidden: cn("invisible", defaultClassNames.hidden),
|
||||||
|
...classNames,
|
||||||
|
}}
|
||||||
|
components={{
|
||||||
|
Root: ({ className, rootRef, ...props }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="calendar"
|
||||||
|
ref={rootRef}
|
||||||
|
className={cn(className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
Chevron: ({ className, orientation, ...props }) => {
|
||||||
|
if (orientation === "left") {
|
||||||
|
return (
|
||||||
|
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orientation === "right") {
|
||||||
|
return (
|
||||||
|
<ChevronRightIcon
|
||||||
|
className={cn("size-4", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChevronDownIcon className={cn("size-4", className)} {...props} />
|
||||||
|
)
|
||||||
|
},
|
||||||
|
DayButton: CalendarDayButton,
|
||||||
|
WeekNumber: ({ children, ...props }) => {
|
||||||
|
return (
|
||||||
|
<td {...props}>
|
||||||
|
<div className="flex size-(--cell-size) items-center justify-center text-center">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
...components,
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CalendarDayButton({
|
||||||
|
className,
|
||||||
|
day,
|
||||||
|
modifiers,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DayButton>) {
|
||||||
|
const defaultClassNames = getDefaultClassNames()
|
||||||
|
|
||||||
|
const ref = React.useRef<HTMLButtonElement>(null)
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (modifiers.focused) ref.current?.focus()
|
||||||
|
}, [modifiers.focused])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
ref={ref}
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
data-day={day.date.toLocaleDateString()}
|
||||||
|
data-selected-single={
|
||||||
|
modifiers.selected &&
|
||||||
|
!modifiers.range_start &&
|
||||||
|
!modifiers.range_end &&
|
||||||
|
!modifiers.range_middle
|
||||||
|
}
|
||||||
|
data-range-start={modifiers.range_start}
|
||||||
|
data-range-end={modifiers.range_end}
|
||||||
|
data-range-middle={modifiers.range_middle}
|
||||||
|
className={cn(
|
||||||
|
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70",
|
||||||
|
defaultClassNames.day,
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Calendar, CalendarDayButton }
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import {
|
||||||
|
ChevronLeftIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
|
MoreHorizontalIcon,
|
||||||
|
} from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Button, buttonVariants } from "@/components/ui/button"
|
||||||
|
|
||||||
|
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
|
||||||
|
return (
|
||||||
|
<nav
|
||||||
|
role="navigation"
|
||||||
|
aria-label="pagination"
|
||||||
|
data-slot="pagination"
|
||||||
|
className={cn("mx-auto flex w-full justify-center", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"ul">) {
|
||||||
|
return (
|
||||||
|
<ul
|
||||||
|
data-slot="pagination-content"
|
||||||
|
className={cn("flex flex-row items-center gap-1", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
|
||||||
|
return <li data-slot="pagination-item" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaginationLinkProps = {
|
||||||
|
isActive?: boolean
|
||||||
|
} & Pick<React.ComponentProps<typeof Button>, "size"> &
|
||||||
|
React.ComponentProps<"a">
|
||||||
|
|
||||||
|
function PaginationLink({
|
||||||
|
className,
|
||||||
|
isActive,
|
||||||
|
size = "icon",
|
||||||
|
...props
|
||||||
|
}: PaginationLinkProps) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
aria-current={isActive ? "page" : undefined}
|
||||||
|
data-slot="pagination-link"
|
||||||
|
data-active={isActive}
|
||||||
|
className={cn(
|
||||||
|
buttonVariants({
|
||||||
|
variant: isActive ? "outline" : "ghost",
|
||||||
|
size,
|
||||||
|
}),
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationPrevious({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PaginationLink>) {
|
||||||
|
return (
|
||||||
|
<PaginationLink
|
||||||
|
aria-label="Go to previous page"
|
||||||
|
size="default"
|
||||||
|
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronLeftIcon />
|
||||||
|
<span className="hidden sm:block">Previous</span>
|
||||||
|
</PaginationLink>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationNext({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PaginationLink>) {
|
||||||
|
return (
|
||||||
|
<PaginationLink
|
||||||
|
aria-label="Go to next page"
|
||||||
|
size="default"
|
||||||
|
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="hidden sm:block">Next</span>
|
||||||
|
<ChevronRightIcon />
|
||||||
|
</PaginationLink>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationEllipsis({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
data-slot="pagination-ellipsis"
|
||||||
|
className={cn("flex size-9 items-center justify-center", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<MoreHorizontalIcon className="size-4" />
|
||||||
|
<span className="sr-only">More pages</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Pagination,
|
||||||
|
PaginationContent,
|
||||||
|
PaginationLink,
|
||||||
|
PaginationItem,
|
||||||
|
PaginationPrevious,
|
||||||
|
PaginationNext,
|
||||||
|
PaginationEllipsis,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Popover({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
||||||
|
return <PopoverPrimitive.Root data-slot="popover" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
||||||
|
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverContent({
|
||||||
|
className,
|
||||||
|
align = "center",
|
||||||
|
sideOffset = 4,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<PopoverPrimitive.Portal>
|
||||||
|
<PopoverPrimitive.Content
|
||||||
|
data-slot="popover-content"
|
||||||
|
align={align}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</PopoverPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverAnchor({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
||||||
|
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
"use client";
|
||||||
import Swal from "sweetalert2";
|
import Swal from "sweetalert2";
|
||||||
import withReactContent from "sweetalert2-react-content";
|
import withReactContent from "sweetalert2-react-content";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
// Utility functions for chat history management
|
// Utility functions for chat history management
|
||||||
|
import Cookies from "js-cookie";
|
||||||
|
const token = Cookies.get("access_token");
|
||||||
|
|
||||||
export interface ChatMessage {
|
export interface ChatMessage {
|
||||||
type: 'user' | 'assistant';
|
type: "user" | "assistant";
|
||||||
content: string;
|
content: string;
|
||||||
created_at?: string;
|
created_at?: string;
|
||||||
timestamp?: Date;
|
timestamp?: Date;
|
||||||
|
|
@ -9,13 +11,13 @@ export interface ChatMessage {
|
||||||
|
|
||||||
export interface ChatSessionDetail {
|
export interface ChatSessionDetail {
|
||||||
id: number;
|
id: number;
|
||||||
session_id: string;
|
sessionId: string;
|
||||||
user_id: string;
|
userId: string;
|
||||||
agent_id: string;
|
agentId: string;
|
||||||
title: string;
|
title: string;
|
||||||
created_at: string;
|
createdAt: string;
|
||||||
updated_at: string;
|
updatedAt: string;
|
||||||
message_count: number;
|
messageCount: number;
|
||||||
status: string;
|
status: string;
|
||||||
messages: ChatMessage[];
|
messages: ChatMessage[];
|
||||||
}
|
}
|
||||||
|
|
@ -28,95 +30,119 @@ export interface SessionDetailResponse {
|
||||||
|
|
||||||
export interface ChatSession {
|
export interface ChatSession {
|
||||||
id: number;
|
id: number;
|
||||||
session_id: string;
|
sessionId: string;
|
||||||
user_id: string;
|
userId: string;
|
||||||
agent_id: string;
|
agentId: string;
|
||||||
title: string;
|
title: string;
|
||||||
created_at: string;
|
createdAt: string;
|
||||||
updated_at: string;
|
updatedAt: string;
|
||||||
message_count: number;
|
messageCount: number;
|
||||||
status: string;
|
status: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChatHistoryResponse {
|
|
||||||
sessions: ChatSession[];
|
|
||||||
total: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: ChatDetailResponse is no longer needed since we only get sessions list
|
// Note: ChatDetailResponse is no longer needed since we only get sessions list
|
||||||
|
|
||||||
// Save chat history
|
// Save chat history
|
||||||
export async function saveChatHistory(data: {
|
export async function saveChatHistory(data: {
|
||||||
user_id: string;
|
user_id: number;
|
||||||
agent_id: string;
|
agent_id: string;
|
||||||
session_id: string;
|
session_id: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
messages?: ChatMessage[];
|
messages?: ChatMessage[];
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/internal-api/chat-history', {
|
const response = await fetch(
|
||||||
method: 'POST',
|
"https://narasiahli.com/api/ai-chat/sessions",
|
||||||
headers: {
|
{
|
||||||
'Content-Type': 'application/json',
|
method: "POST",
|
||||||
},
|
headers: {
|
||||||
body: JSON.stringify({
|
"Content-Type": "application/json",
|
||||||
...data,
|
Authorization: `Bearer ${token}`,
|
||||||
messages: data.messages?.map(msg => ({
|
},
|
||||||
type: msg.type,
|
body: JSON.stringify({
|
||||||
content: msg.content
|
agentId: data.agent_id,
|
||||||
}))
|
sessionId: data.session_id,
|
||||||
}),
|
title: data.title,
|
||||||
});
|
}),
|
||||||
|
}
|
||||||
if (!response.ok) {
|
);
|
||||||
throw new Error('Failed to save chat history');
|
if (data.messages) {
|
||||||
|
for (const element of data.messages) {
|
||||||
|
const response2 = await fetch(
|
||||||
|
"https://narasiahli.com/api/ai-chat/sessions/messages",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
content: element.content,
|
||||||
|
messageType: element.type,
|
||||||
|
sessionId: data.session_id,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if (!response.ok) {
|
||||||
|
// throw new Error("Failed to save chat history");
|
||||||
|
// }
|
||||||
|
|
||||||
return await response.json();
|
return await response.json();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving chat history:', error);
|
console.error("Error saving chat history:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get chat history
|
// Get chat history
|
||||||
export async function getChatHistory(user_id: string): Promise<ChatHistoryResponse> {
|
export async function getChatHistory(user_id: string) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/internal-api/chat-history?user_id=${user_id}`, {
|
const response = await fetch(
|
||||||
method: 'GET',
|
"https://narasiahli.com/api/ai-chat/sessions",
|
||||||
headers: {
|
{
|
||||||
'Content-Type': 'application/json',
|
method: "GET",
|
||||||
},
|
headers: {
|
||||||
});
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to get chat history');
|
throw new Error("Failed to get chat history");
|
||||||
}
|
}
|
||||||
|
|
||||||
return await response.json();
|
return await response.json();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting chat history:', error);
|
console.error("Error getting chat history:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get session details
|
// Get session details
|
||||||
export async function getSessionDetail(id: string): Promise<SessionDetailResponse> {
|
export async function getSessionDetail(id: string) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/internal-api/chat-history/${id}`, {
|
const response = await fetch(
|
||||||
method: 'GET',
|
`https://narasiahli.com/api/ai-chat/sessions/${id}`,
|
||||||
headers: {
|
{
|
||||||
'Content-Type': 'application/json',
|
method: "GET",
|
||||||
},
|
headers: {
|
||||||
});
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to get session details');
|
throw new Error("Failed to get session details");
|
||||||
}
|
}
|
||||||
|
|
||||||
return await response.json();
|
return await response.json();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting session details:', error);
|
console.error("Error getting session details:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -124,20 +150,24 @@ export async function getSessionDetail(id: string): Promise<SessionDetailRespons
|
||||||
// Delete chat session
|
// Delete chat session
|
||||||
export async function deleteChatSession(id: string, user_id: string) {
|
export async function deleteChatSession(id: string, user_id: string) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/internal-api/chat-history/${id}?user_id=${user_id}`, {
|
const response = await fetch(
|
||||||
method: 'DELETE',
|
`https://narasiahli.com/api/ai-chat/sessions/${id}`,
|
||||||
headers: {
|
{
|
||||||
'Content-Type': 'application/json',
|
method: "DELETE",
|
||||||
},
|
headers: {
|
||||||
});
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to delete chat session');
|
throw new Error("Failed to delete chat session");
|
||||||
}
|
}
|
||||||
|
|
||||||
return await response.json();
|
return await response.json();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting chat session:', error);
|
console.error("Error deleting chat session:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
|
|
@ -15,19 +15,24 @@
|
||||||
"@fullcalendar/interaction": "^6.1.19",
|
"@fullcalendar/interaction": "^6.1.19",
|
||||||
"@fullcalendar/react": "^6.1.19",
|
"@fullcalendar/react": "^6.1.19",
|
||||||
"@fullcalendar/timegrid": "^6.1.19",
|
"@fullcalendar/timegrid": "^6.1.19",
|
||||||
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@radix-ui/react-checkbox": "^1.3.2",
|
"@radix-ui/react-checkbox": "^1.3.2",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
|
"@radix-ui/react-popover": "^1.1.15",
|
||||||
"@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-tabs": "^1.1.13",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"better-sqlite3": "^11.10.0",
|
"better-sqlite3": "^11.10.0",
|
||||||
"chart.js": "^4.5.0",
|
"chart.js": "^4.5.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"framer-motion": "^12.23.6",
|
"framer-motion": "^12.23.6",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
|
|
@ -35,11 +40,17 @@
|
||||||
"next": "15.4.3",
|
"next": "15.4.3",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-chartjs-2": "^5.3.0",
|
"react-chartjs-2": "^5.3.0",
|
||||||
|
"react-day-picker": "^9.10.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
|
"react-hook-form": "^7.63.0",
|
||||||
|
"react-hot-toast": "^2.6.0",
|
||||||
|
"react-is": "^19.1.1",
|
||||||
"recharts": "^3.1.2",
|
"recharts": "^3.1.2",
|
||||||
|
"sweetalert2": "^11.23.0",
|
||||||
"sweetalert2-react-content": "^5.1.0",
|
"sweetalert2-react-content": "^5.1.0",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"uuid": "^11.1.0"
|
"uuid": "^11.1.0",
|
||||||
|
"zod": "^4.1.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
|
|
||||||
224
pnpm-lock.yaml
224
pnpm-lock.yaml
|
|
@ -8,12 +8,18 @@ importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@hookform/resolvers':
|
||||||
|
specifier: ^5.2.2
|
||||||
|
version: 5.2.2(react-hook-form@7.62.0(react@19.1.0))
|
||||||
'@radix-ui/react-checkbox':
|
'@radix-ui/react-checkbox':
|
||||||
specifier: ^1.3.2
|
specifier: ^1.3.2
|
||||||
version: 1.3.2(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 1.3.2(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
'@radix-ui/react-label':
|
'@radix-ui/react-label':
|
||||||
specifier: ^2.1.7
|
specifier: ^2.1.7
|
||||||
version: 2.1.7(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 2.1.7(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
'@radix-ui/react-popover':
|
||||||
|
specifier: ^1.1.15
|
||||||
|
version: 1.1.15(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
'@radix-ui/react-radio-group':
|
'@radix-ui/react-radio-group':
|
||||||
specifier: ^1.3.7
|
specifier: ^1.3.7
|
||||||
version: 1.3.7(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 1.3.7(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
|
@ -38,6 +44,9 @@ importers:
|
||||||
clsx:
|
clsx:
|
||||||
specifier: ^2.1.1
|
specifier: ^2.1.1
|
||||||
version: 2.1.1
|
version: 2.1.1
|
||||||
|
date-fns:
|
||||||
|
specifier: ^4.1.0
|
||||||
|
version: 4.1.0
|
||||||
embla-carousel-react:
|
embla-carousel-react:
|
||||||
specifier: ^8.6.0
|
specifier: ^8.6.0
|
||||||
version: 8.6.0(react@19.1.0)
|
version: 8.6.0(react@19.1.0)
|
||||||
|
|
@ -56,18 +65,27 @@ importers:
|
||||||
react:
|
react:
|
||||||
specifier: 19.1.0
|
specifier: 19.1.0
|
||||||
version: 19.1.0
|
version: 19.1.0
|
||||||
|
react-day-picker:
|
||||||
|
specifier: ^9.10.0
|
||||||
|
version: 9.10.0(react@19.1.0)
|
||||||
react-dom:
|
react-dom:
|
||||||
specifier: 19.1.0
|
specifier: 19.1.0
|
||||||
version: 19.1.0(react@19.1.0)
|
version: 19.1.0(react@19.1.0)
|
||||||
|
sweetalert2:
|
||||||
|
specifier: ^11.23.0
|
||||||
|
version: 11.23.0
|
||||||
sweetalert2-react-content:
|
sweetalert2-react-content:
|
||||||
specifier: ^5.1.0
|
specifier: ^5.1.0
|
||||||
version: 5.1.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sweetalert2@11.22.2)
|
version: 5.1.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sweetalert2@11.23.0)
|
||||||
tailwind-merge:
|
tailwind-merge:
|
||||||
specifier: ^3.3.1
|
specifier: ^3.3.1
|
||||||
version: 3.3.1
|
version: 3.3.1
|
||||||
uuid:
|
uuid:
|
||||||
specifier: ^11.1.0
|
specifier: ^11.1.0
|
||||||
version: 11.1.0
|
version: 11.1.0
|
||||||
|
zod:
|
||||||
|
specifier: ^4.1.9
|
||||||
|
version: 4.1.9
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@tailwindcss/postcss':
|
'@tailwindcss/postcss':
|
||||||
specifier: ^4
|
specifier: ^4
|
||||||
|
|
@ -104,6 +122,9 @@ packages:
|
||||||
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
|
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
|
|
||||||
|
'@date-fns/tz@1.4.1':
|
||||||
|
resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==}
|
||||||
|
|
||||||
'@emnapi/runtime@1.4.5':
|
'@emnapi/runtime@1.4.5':
|
||||||
resolution: {integrity: sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==}
|
resolution: {integrity: sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==}
|
||||||
|
|
||||||
|
|
@ -122,6 +143,11 @@ packages:
|
||||||
'@floating-ui/utils@0.2.10':
|
'@floating-ui/utils@0.2.10':
|
||||||
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
|
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
|
||||||
|
|
||||||
|
'@hookform/resolvers@5.2.2':
|
||||||
|
resolution: {integrity: sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==}
|
||||||
|
peerDependencies:
|
||||||
|
react-hook-form: ^7.55.0
|
||||||
|
|
||||||
'@img/sharp-darwin-arm64@0.34.3':
|
'@img/sharp-darwin-arm64@0.34.3':
|
||||||
resolution: {integrity: sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==}
|
resolution: {integrity: sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
|
@ -318,6 +344,9 @@ packages:
|
||||||
'@radix-ui/primitive@1.1.2':
|
'@radix-ui/primitive@1.1.2':
|
||||||
resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==}
|
resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==}
|
||||||
|
|
||||||
|
'@radix-ui/primitive@1.1.3':
|
||||||
|
resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
|
||||||
|
|
||||||
'@radix-ui/react-arrow@1.1.7':
|
'@radix-ui/react-arrow@1.1.7':
|
||||||
resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==}
|
resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -397,6 +426,19 @@ packages:
|
||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-dismissable-layer@1.1.11':
|
||||||
|
resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==}
|
||||||
|
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
|
||||||
|
|
||||||
'@radix-ui/react-focus-guards@1.1.2':
|
'@radix-ui/react-focus-guards@1.1.2':
|
||||||
resolution: {integrity: sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==}
|
resolution: {integrity: sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -406,6 +448,15 @@ packages:
|
||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-focus-guards@1.1.3':
|
||||||
|
resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-focus-scope@1.1.7':
|
'@radix-ui/react-focus-scope@1.1.7':
|
||||||
resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==}
|
resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -441,6 +492,19 @@ packages:
|
||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-popover@1.1.15':
|
||||||
|
resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==}
|
||||||
|
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
|
||||||
|
|
||||||
'@radix-ui/react-popper@1.2.7':
|
'@radix-ui/react-popper@1.2.7':
|
||||||
resolution: {integrity: sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==}
|
resolution: {integrity: sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -454,6 +518,19 @@ packages:
|
||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-popper@1.2.8':
|
||||||
|
resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==}
|
||||||
|
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
|
||||||
|
|
||||||
'@radix-ui/react-portal@1.1.9':
|
'@radix-ui/react-portal@1.1.9':
|
||||||
resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==}
|
resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -480,6 +557,19 @@ packages:
|
||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-presence@1.1.5':
|
||||||
|
resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==}
|
||||||
|
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
|
||||||
|
|
||||||
'@radix-ui/react-primitive@2.1.3':
|
'@radix-ui/react-primitive@2.1.3':
|
||||||
resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==}
|
resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -629,6 +719,9 @@ packages:
|
||||||
'@radix-ui/rect@1.1.1':
|
'@radix-ui/rect@1.1.1':
|
||||||
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
|
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
|
||||||
|
|
||||||
|
'@standard-schema/utils@0.3.0':
|
||||||
|
resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
|
||||||
|
|
||||||
'@swc/helpers@0.5.15':
|
'@swc/helpers@0.5.15':
|
||||||
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
|
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
|
||||||
|
|
||||||
|
|
@ -807,6 +900,12 @@ packages:
|
||||||
csstype@3.1.3:
|
csstype@3.1.3:
|
||||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||||
|
|
||||||
|
date-fns-jalali@4.1.0-0:
|
||||||
|
resolution: {integrity: sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==}
|
||||||
|
|
||||||
|
date-fns@4.1.0:
|
||||||
|
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
|
||||||
|
|
||||||
decompress-response@6.0.0:
|
decompress-response@6.0.0:
|
||||||
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
|
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
@ -1135,11 +1234,23 @@ packages:
|
||||||
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
|
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
react-day-picker@9.10.0:
|
||||||
|
resolution: {integrity: sha512-tedecLSd+fpSN+J08601MaMsf122nxtqZXxB6lwX37qFoLtuPNuRJN8ylxFjLhyJS1kaLfAqL1GUkSLd2BMrpQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16.8.0'
|
||||||
|
|
||||||
react-dom@19.1.0:
|
react-dom@19.1.0:
|
||||||
resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==}
|
resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^19.1.0
|
react: ^19.1.0
|
||||||
|
|
||||||
|
react-hook-form@7.62.0:
|
||||||
|
resolution: {integrity: sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==}
|
||||||
|
engines: {node: '>=18.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8.0 || ^17 || ^18 || ^19
|
||||||
|
|
||||||
react-remove-scroll-bar@2.3.8:
|
react-remove-scroll-bar@2.3.8:
|
||||||
resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==}
|
resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
@ -1233,8 +1344,8 @@ packages:
|
||||||
react-dom: ^18.0.0 || ^19.0.0
|
react-dom: ^18.0.0 || ^19.0.0
|
||||||
sweetalert2: ^11.0.0
|
sweetalert2: ^11.0.0
|
||||||
|
|
||||||
sweetalert2@11.22.2:
|
sweetalert2@11.23.0:
|
||||||
resolution: {integrity: sha512-GFQGzw8ZXF23PO79WMAYXLl4zYmLiaKqYJwcp5eBF07wiI5BYPbZtKi2pcvVmfUQK+FqL1risJAMxugcPbGIyg==}
|
resolution: {integrity: sha512-cKzzbC3C1sIs7o9XAMw4E8F9kBtGXsBDUsd2JZ8JM/dqa+nzWwSGM+9LLYILZWzWHzX9W+HJNHyBlbHPVS/krw==}
|
||||||
|
|
||||||
tailwind-merge@3.3.1:
|
tailwind-merge@3.3.1:
|
||||||
resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==}
|
resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==}
|
||||||
|
|
@ -1308,6 +1419,9 @@ packages:
|
||||||
resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
|
resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
zod@4.1.9:
|
||||||
|
resolution: {integrity: sha512-HI32jTq0AUAC125z30E8bQNz0RQ+9Uc+4J7V97gLYjZVKRjeydPgGt6dvQzFrav7MYOUGFqqOGiHpA/fdbd0cQ==}
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
'@alloc/quick-lru@5.2.0': {}
|
'@alloc/quick-lru@5.2.0': {}
|
||||||
|
|
@ -1317,6 +1431,8 @@ snapshots:
|
||||||
'@jridgewell/gen-mapping': 0.3.12
|
'@jridgewell/gen-mapping': 0.3.12
|
||||||
'@jridgewell/trace-mapping': 0.3.29
|
'@jridgewell/trace-mapping': 0.3.29
|
||||||
|
|
||||||
|
'@date-fns/tz@1.4.1': {}
|
||||||
|
|
||||||
'@emnapi/runtime@1.4.5':
|
'@emnapi/runtime@1.4.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
@ -1339,6 +1455,11 @@ snapshots:
|
||||||
|
|
||||||
'@floating-ui/utils@0.2.10': {}
|
'@floating-ui/utils@0.2.10': {}
|
||||||
|
|
||||||
|
'@hookform/resolvers@5.2.2(react-hook-form@7.62.0(react@19.1.0))':
|
||||||
|
dependencies:
|
||||||
|
'@standard-schema/utils': 0.3.0
|
||||||
|
react-hook-form: 7.62.0(react@19.1.0)
|
||||||
|
|
||||||
'@img/sharp-darwin-arm64@0.34.3':
|
'@img/sharp-darwin-arm64@0.34.3':
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@img/sharp-libvips-darwin-arm64': 1.2.0
|
'@img/sharp-libvips-darwin-arm64': 1.2.0
|
||||||
|
|
@ -1473,6 +1594,8 @@ snapshots:
|
||||||
|
|
||||||
'@radix-ui/primitive@1.1.2': {}
|
'@radix-ui/primitive@1.1.2': {}
|
||||||
|
|
||||||
|
'@radix-ui/primitive@1.1.3': {}
|
||||||
|
|
||||||
'@radix-ui/react-arrow@1.1.7(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
'@radix-ui/react-arrow@1.1.7(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
|
@ -1541,12 +1664,31 @@ snapshots:
|
||||||
'@types/react': 19.1.9
|
'@types/react': 19.1.9
|
||||||
'@types/react-dom': 19.1.7(@types/react@19.1.9)
|
'@types/react-dom': 19.1.7(@types/react@19.1.9)
|
||||||
|
|
||||||
|
'@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/primitive': 1.1.3
|
||||||
|
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.9)(react@19.1.0)
|
||||||
|
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.9)(react@19.1.0)
|
||||||
|
'@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.1.9)(react@19.1.0)
|
||||||
|
react: 19.1.0
|
||||||
|
react-dom: 19.1.0(react@19.1.0)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.1.9
|
||||||
|
'@types/react-dom': 19.1.7(@types/react@19.1.9)
|
||||||
|
|
||||||
'@radix-ui/react-focus-guards@1.1.2(@types/react@19.1.9)(react@19.1.0)':
|
'@radix-ui/react-focus-guards@1.1.2(@types/react@19.1.9)(react@19.1.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.1.9
|
'@types/react': 19.1.9
|
||||||
|
|
||||||
|
'@radix-ui/react-focus-guards@1.1.3(@types/react@19.1.9)(react@19.1.0)':
|
||||||
|
dependencies:
|
||||||
|
react: 19.1.0
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.1.9
|
||||||
|
|
||||||
'@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
'@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.9)(react@19.1.0)
|
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.9)(react@19.1.0)
|
||||||
|
|
@ -1574,6 +1716,29 @@ snapshots:
|
||||||
'@types/react': 19.1.9
|
'@types/react': 19.1.9
|
||||||
'@types/react-dom': 19.1.7(@types/react@19.1.9)
|
'@types/react-dom': 19.1.7(@types/react@19.1.9)
|
||||||
|
|
||||||
|
'@radix-ui/react-popover@1.1.15(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/primitive': 1.1.3
|
||||||
|
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.9)(react@19.1.0)
|
||||||
|
'@radix-ui/react-context': 1.1.2(@types/react@19.1.9)(react@19.1.0)
|
||||||
|
'@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
'@radix-ui/react-focus-guards': 1.1.3(@types/react@19.1.9)(react@19.1.0)
|
||||||
|
'@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
'@radix-ui/react-id': 1.1.1(@types/react@19.1.9)(react@19.1.0)
|
||||||
|
'@radix-ui/react-popper': 1.2.8(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
'@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
'@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
'@radix-ui/react-slot': 1.2.3(@types/react@19.1.9)(react@19.1.0)
|
||||||
|
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.9)(react@19.1.0)
|
||||||
|
aria-hidden: 1.2.6
|
||||||
|
react: 19.1.0
|
||||||
|
react-dom: 19.1.0(react@19.1.0)
|
||||||
|
react-remove-scroll: 2.7.1(@types/react@19.1.9)(react@19.1.0)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.1.9
|
||||||
|
'@types/react-dom': 19.1.7(@types/react@19.1.9)
|
||||||
|
|
||||||
'@radix-ui/react-popper@1.2.7(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
'@radix-ui/react-popper@1.2.7(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@floating-ui/react-dom': 2.1.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
'@floating-ui/react-dom': 2.1.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
|
@ -1592,6 +1757,24 @@ snapshots:
|
||||||
'@types/react': 19.1.9
|
'@types/react': 19.1.9
|
||||||
'@types/react-dom': 19.1.7(@types/react@19.1.9)
|
'@types/react-dom': 19.1.7(@types/react@19.1.9)
|
||||||
|
|
||||||
|
'@radix-ui/react-popper@1.2.8(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||||
|
dependencies:
|
||||||
|
'@floating-ui/react-dom': 2.1.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
'@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.9)(react@19.1.0)
|
||||||
|
'@radix-ui/react-context': 1.1.2(@types/react@19.1.9)(react@19.1.0)
|
||||||
|
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.9)(react@19.1.0)
|
||||||
|
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.9)(react@19.1.0)
|
||||||
|
'@radix-ui/react-use-rect': 1.1.1(@types/react@19.1.9)(react@19.1.0)
|
||||||
|
'@radix-ui/react-use-size': 1.1.1(@types/react@19.1.9)(react@19.1.0)
|
||||||
|
'@radix-ui/rect': 1.1.1
|
||||||
|
react: 19.1.0
|
||||||
|
react-dom: 19.1.0(react@19.1.0)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.1.9
|
||||||
|
'@types/react-dom': 19.1.7(@types/react@19.1.9)
|
||||||
|
|
||||||
'@radix-ui/react-portal@1.1.9(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
'@radix-ui/react-portal@1.1.9(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
|
@ -1612,6 +1795,16 @@ snapshots:
|
||||||
'@types/react': 19.1.9
|
'@types/react': 19.1.9
|
||||||
'@types/react-dom': 19.1.7(@types/react@19.1.9)
|
'@types/react-dom': 19.1.7(@types/react@19.1.9)
|
||||||
|
|
||||||
|
'@radix-ui/react-presence@1.1.5(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.9)(react@19.1.0)
|
||||||
|
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.9)(react@19.1.0)
|
||||||
|
react: 19.1.0
|
||||||
|
react-dom: 19.1.0(react@19.1.0)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.1.9
|
||||||
|
'@types/react-dom': 19.1.7(@types/react@19.1.9)
|
||||||
|
|
||||||
'@radix-ui/react-primitive@2.1.3(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
'@radix-ui/react-primitive@2.1.3(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-slot': 1.2.3(@types/react@19.1.9)(react@19.1.0)
|
'@radix-ui/react-slot': 1.2.3(@types/react@19.1.9)(react@19.1.0)
|
||||||
|
|
@ -1757,6 +1950,8 @@ snapshots:
|
||||||
|
|
||||||
'@radix-ui/rect@1.1.1': {}
|
'@radix-ui/rect@1.1.1': {}
|
||||||
|
|
||||||
|
'@standard-schema/utils@0.3.0': {}
|
||||||
|
|
||||||
'@swc/helpers@0.5.15':
|
'@swc/helpers@0.5.15':
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
@ -1932,6 +2127,10 @@ snapshots:
|
||||||
|
|
||||||
csstype@3.1.3: {}
|
csstype@3.1.3: {}
|
||||||
|
|
||||||
|
date-fns-jalali@4.1.0-0: {}
|
||||||
|
|
||||||
|
date-fns@4.1.0: {}
|
||||||
|
|
||||||
decompress-response@6.0.0:
|
decompress-response@6.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
mimic-response: 3.1.0
|
mimic-response: 3.1.0
|
||||||
|
|
@ -2221,11 +2420,22 @@ snapshots:
|
||||||
minimist: 1.2.8
|
minimist: 1.2.8
|
||||||
strip-json-comments: 2.0.1
|
strip-json-comments: 2.0.1
|
||||||
|
|
||||||
|
react-day-picker@9.10.0(react@19.1.0):
|
||||||
|
dependencies:
|
||||||
|
'@date-fns/tz': 1.4.1
|
||||||
|
date-fns: 4.1.0
|
||||||
|
date-fns-jalali: 4.1.0-0
|
||||||
|
react: 19.1.0
|
||||||
|
|
||||||
react-dom@19.1.0(react@19.1.0):
|
react-dom@19.1.0(react@19.1.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
scheduler: 0.26.0
|
scheduler: 0.26.0
|
||||||
|
|
||||||
|
react-hook-form@7.62.0(react@19.1.0):
|
||||||
|
dependencies:
|
||||||
|
react: 19.1.0
|
||||||
|
|
||||||
react-remove-scroll-bar@2.3.8(@types/react@19.1.9)(react@19.1.0):
|
react-remove-scroll-bar@2.3.8(@types/react@19.1.9)(react@19.1.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
|
|
@ -2323,13 +2533,13 @@ snapshots:
|
||||||
client-only: 0.0.1
|
client-only: 0.0.1
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
|
|
||||||
sweetalert2-react-content@5.1.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sweetalert2@11.22.2):
|
sweetalert2-react-content@5.1.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sweetalert2@11.23.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
react-dom: 19.1.0(react@19.1.0)
|
react-dom: 19.1.0(react@19.1.0)
|
||||||
sweetalert2: 11.22.2
|
sweetalert2: 11.23.0
|
||||||
|
|
||||||
sweetalert2@11.22.2: {}
|
sweetalert2@11.23.0: {}
|
||||||
|
|
||||||
tailwind-merge@3.3.1: {}
|
tailwind-merge@3.3.1: {}
|
||||||
|
|
||||||
|
|
@ -2395,3 +2605,5 @@ snapshots:
|
||||||
wrappy@1.0.2: {}
|
wrappy@1.0.2: {}
|
||||||
|
|
||||||
yallist@5.0.0: {}
|
yallist@5.0.0: {}
|
||||||
|
|
||||||
|
zod@4.1.9: {}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
import { httpGetInterceptor, httpPostInterceptor, httpPutInterceptor } from "./http-config/http-interceptor-services";
|
||||||
|
|
||||||
|
export interface EbookData {
|
||||||
|
id?: number;
|
||||||
|
title: string;
|
||||||
|
slug: string;
|
||||||
|
description: string;
|
||||||
|
price: number;
|
||||||
|
category: string;
|
||||||
|
tags: string;
|
||||||
|
pageCount: number;
|
||||||
|
language: string;
|
||||||
|
isbn: string;
|
||||||
|
publisher: string;
|
||||||
|
publishedYear: number;
|
||||||
|
isPublished: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EbookDetailResponse {
|
||||||
|
success: boolean;
|
||||||
|
code: number;
|
||||||
|
messages: string[];
|
||||||
|
data: {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
slug: string;
|
||||||
|
description: string;
|
||||||
|
price: number;
|
||||||
|
pdfFilePath: string | null;
|
||||||
|
pdfFileName: string | null;
|
||||||
|
pdfFileSize: number | null;
|
||||||
|
thumbnailPath: string | null;
|
||||||
|
thumbnailName: string | null;
|
||||||
|
authorId: number;
|
||||||
|
authorName: string;
|
||||||
|
authorEmail: string;
|
||||||
|
category: string;
|
||||||
|
tags: string;
|
||||||
|
pageCount: number;
|
||||||
|
language: string;
|
||||||
|
isbn: string;
|
||||||
|
publisher: string;
|
||||||
|
publishedYear: number;
|
||||||
|
downloadCount: number;
|
||||||
|
purchaseCount: number;
|
||||||
|
wishlistCount: number;
|
||||||
|
rating: number;
|
||||||
|
reviewCount: number;
|
||||||
|
statusId: number;
|
||||||
|
isActive: boolean;
|
||||||
|
isPublished: boolean;
|
||||||
|
publishedAt: string | null;
|
||||||
|
createdById: number;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new ebook
|
||||||
|
export async function createEbook(ebookData: EbookData) {
|
||||||
|
const response = await httpPostInterceptor("/ebooks", ebookData);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update existing ebook
|
||||||
|
export async function updateEbook(id: number, ebookData: EbookData) {
|
||||||
|
const response = await httpPutInterceptor(`/ebooks/${id}`, ebookData);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ebook detail
|
||||||
|
export async function getEbookDetail(id: number): Promise<EbookDetailResponse> {
|
||||||
|
const response = await httpGetInterceptor(`/ebooks/${id}`);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload PDF file for ebook
|
||||||
|
export async function uploadEbookPdf(id: number, file: File) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('files', file);
|
||||||
|
|
||||||
|
const response = await httpPostInterceptor(
|
||||||
|
`/ebooks/pdf/${id}`,
|
||||||
|
formData,
|
||||||
|
{
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all ebooks (for listing)
|
||||||
|
export async function getAllEbooks() {
|
||||||
|
const response = await httpGetInterceptor("/ebooks");
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import { httpGet, httpPost } from "./http-config/http-base-services";
|
import { httpGet, httpPost } from "./http-config/http-base-services";
|
||||||
import { httpDeleteInterceptor, httpGetInterceptor, httpPostInterceptor, httpPutInterceptor } from "./http-config/http-interceptor-services";
|
import {
|
||||||
|
httpDeleteInterceptor,
|
||||||
|
httpGetInterceptor,
|
||||||
|
httpPostInterceptor,
|
||||||
|
httpPutInterceptor,
|
||||||
|
} from "./http-config/http-interceptor-services";
|
||||||
|
|
||||||
const token = Cookies.get("access_token");
|
const token = Cookies.get("access_token");
|
||||||
const id = Cookies.get("uie");
|
const id = Cookies.get("uie");
|
||||||
|
|
@ -32,12 +37,12 @@ export async function getDetailMasterUsers(id: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function editMasterUsers(data: any, id: string) {
|
export async function editMasterUsers(data: any, id: string) {
|
||||||
const pathUrl = `/users/${id}`
|
const pathUrl = `/users/${id}`;
|
||||||
return await httpPutInterceptor(pathUrl, data);
|
return await httpPutInterceptor(pathUrl, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteMasterUser(id: string) {
|
export async function deleteMasterUser(id: string) {
|
||||||
const pathUrl = `/users/${id}`
|
const pathUrl = `/users/${id}`;
|
||||||
return await httpDeleteInterceptor(pathUrl);
|
return await httpDeleteInterceptor(pathUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -49,7 +54,7 @@ export async function postSignIn(data: any) {
|
||||||
export async function getProfile(token?: string) {
|
export async function getProfile(token?: string) {
|
||||||
const headers = {
|
const headers = {
|
||||||
"content-type": "application/json",
|
"content-type": "application/json",
|
||||||
"Authorization": `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
};
|
};
|
||||||
const pathUrl = `/users/info`;
|
const pathUrl = `/users/info`;
|
||||||
return await httpGet(pathUrl, headers);
|
return await httpGet(pathUrl, headers);
|
||||||
|
|
@ -60,7 +65,7 @@ export async function updateProfile(data: any) {
|
||||||
return await httpPutInterceptor(pathUrl, data);
|
return await httpPutInterceptor(pathUrl, data);
|
||||||
}
|
}
|
||||||
export async function savePassword(data: any) {
|
export async function savePassword(data: any) {
|
||||||
const pathUrl = `/users/save-password`
|
const pathUrl = `/users/save-password`;
|
||||||
return await httpPostInterceptor(pathUrl, data);
|
return await httpPostInterceptor(pathUrl, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -84,8 +89,8 @@ export async function otpRequest(email: string, name: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function otpValidation(email: string, otpCode: string) {
|
export async function otpValidation(email: string, otpCode: string) {
|
||||||
const pathUrl = `/users/otp-validation`
|
const pathUrl = `/users/otp-validation`;
|
||||||
return await httpPost(pathUrl, { email, otpCode });
|
return await httpPost(pathUrl, { email, otpCode });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function postArticleComment(data: any) {
|
export async function postArticleComment(data: any) {
|
||||||
|
|
@ -111,11 +116,11 @@ export async function getArticleComment(id: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteArticleComment(id: number) {
|
export async function deleteArticleComment(id: number) {
|
||||||
const pathUrl = `/article-comments/${id}`
|
const pathUrl = `/article-comments/${id}`;
|
||||||
return await httpDeleteInterceptor(pathUrl);
|
return await httpDeleteInterceptor(pathUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCsrfToken() {
|
export async function getCsrfToken() {
|
||||||
const pathUrl = "csrf-token";
|
const pathUrl = "csrf-token";
|
||||||
return httpGet(pathUrl);
|
return httpGet(pathUrl);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
import {
|
||||||
|
httpGetInterceptor,
|
||||||
|
httpPostInterceptor,
|
||||||
|
} from "./http-config/http-interceptor-services";
|
||||||
|
|
||||||
|
export interface ScheduleData {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
summary: string;
|
||||||
|
scheduled_at: string;
|
||||||
|
duration: number;
|
||||||
|
chat_session_id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScheduleChatSessionData {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
userIds: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScheduleResponse {
|
||||||
|
error: boolean;
|
||||||
|
message: string;
|
||||||
|
data: {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
summary: string;
|
||||||
|
scheduled_at: string;
|
||||||
|
duration: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create schedule
|
||||||
|
export async function createSchedule(
|
||||||
|
scheduleData: ScheduleData
|
||||||
|
): Promise<ScheduleResponse> {
|
||||||
|
const response = await httpPostInterceptor("/chat/schedules", scheduleData);
|
||||||
|
return response?.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload schedule files
|
||||||
|
export async function uploadScheduleFiles(
|
||||||
|
scheduleId: number,
|
||||||
|
files: File[]
|
||||||
|
): Promise<any> {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
files.forEach((file) => {
|
||||||
|
formData.append("files", file);
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await httpPostInterceptor(
|
||||||
|
`/chat/schedule-files/${scheduleId}`,
|
||||||
|
formData,
|
||||||
|
{
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createChatSessionId(
|
||||||
|
scheduleChatData: ScheduleChatSessionData
|
||||||
|
) {
|
||||||
|
const response = await httpPostInterceptor(
|
||||||
|
"/chat/sessions",
|
||||||
|
scheduleChatData
|
||||||
|
);
|
||||||
|
return response?.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSchedulesData(schedule: any) {
|
||||||
|
const response = await httpGetInterceptor("/chat/schedules");
|
||||||
|
return response?.data;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,129 @@
|
||||||
|
import {
|
||||||
|
httpGetInterceptor,
|
||||||
|
httpPostInterceptor,
|
||||||
|
httpPutInterceptor,
|
||||||
|
} from "./http-config/http-interceptor-services";
|
||||||
|
|
||||||
|
export interface UserData {
|
||||||
|
id?: number;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
fullname: string;
|
||||||
|
address: string;
|
||||||
|
phoneNumber: string;
|
||||||
|
whatsappNumber: string;
|
||||||
|
password: string;
|
||||||
|
dateOfBirth: string;
|
||||||
|
genderType: "male" | "female";
|
||||||
|
degree?: string;
|
||||||
|
userLevelId: number;
|
||||||
|
userRoleId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkHistoryData {
|
||||||
|
userId: number;
|
||||||
|
companyName: string;
|
||||||
|
jobTitle: string;
|
||||||
|
startDate: string;
|
||||||
|
endDate: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EducationHistoryData {
|
||||||
|
userId: number;
|
||||||
|
schoolName: string;
|
||||||
|
major: string;
|
||||||
|
educationLevel: string;
|
||||||
|
graduationYear: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserDetailResponse {
|
||||||
|
success: boolean;
|
||||||
|
code: number;
|
||||||
|
messages: string[];
|
||||||
|
data: {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
fullname: string;
|
||||||
|
address: string;
|
||||||
|
phoneNumber: string;
|
||||||
|
work_type: string | null;
|
||||||
|
genderType: string;
|
||||||
|
identity_type: string | null;
|
||||||
|
identity_group: string | null;
|
||||||
|
identity_group_number: string | null;
|
||||||
|
identity_number: string | null;
|
||||||
|
dateOfBirth: string;
|
||||||
|
last_education: string | null;
|
||||||
|
degree: string | null;
|
||||||
|
whatsappNumber: string;
|
||||||
|
last_job_title: string | null;
|
||||||
|
user_role_id: number;
|
||||||
|
user_level_id: number;
|
||||||
|
user_levels: any;
|
||||||
|
keycloak_id: string;
|
||||||
|
status_id: number;
|
||||||
|
created_by_id: number;
|
||||||
|
profile_picture_path: string | null;
|
||||||
|
temp_password: string;
|
||||||
|
is_email_updated: boolean;
|
||||||
|
is_active: boolean;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new user
|
||||||
|
export async function createUser(userData: UserData) {
|
||||||
|
const response = await httpPostInterceptor("/users", userData);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update existing user
|
||||||
|
export async function updateUser(id: number, userData: Partial<UserData>) {
|
||||||
|
const response = await httpPutInterceptor(`/users/${id}`, userData);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user detail
|
||||||
|
export async function getUserDetail(id: number): Promise<UserDetailResponse> {
|
||||||
|
const response = await httpGetInterceptor(`/users/detail/${id}`);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all users
|
||||||
|
export async function getAllUsers() {
|
||||||
|
const response = await httpGetInterceptor("/users");
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create work history
|
||||||
|
export async function createWorkHistory(workData: WorkHistoryData) {
|
||||||
|
const response = await httpPostInterceptor("/work-history", workData);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create education history
|
||||||
|
export async function createEducationHistory(
|
||||||
|
educationData: EducationHistoryData
|
||||||
|
) {
|
||||||
|
const response = await httpPostInterceptor(
|
||||||
|
"/education-history",
|
||||||
|
educationData
|
||||||
|
);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user work history
|
||||||
|
export async function getUserWorkHistory(userId: number) {
|
||||||
|
const response = await httpGetInterceptor(`/work-history/user/${userId}`);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user education history
|
||||||
|
export async function getUserEducationHistory(userId: number) {
|
||||||
|
const response = await httpGetInterceptor(
|
||||||
|
`/education-history/user/${userId}`
|
||||||
|
);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,172 @@
|
||||||
|
import Cookies from "js-cookie";
|
||||||
|
import CryptoJS from "crypto-js";
|
||||||
|
|
||||||
|
export function convertDateFormat(dateString: string) {
|
||||||
|
var date = new Date(dateString);
|
||||||
|
|
||||||
|
var day = date.getDate();
|
||||||
|
var month = date.getMonth() + 1;
|
||||||
|
var year = date.getFullYear();
|
||||||
|
var hours = date.getHours();
|
||||||
|
var minutes = date.getMinutes();
|
||||||
|
|
||||||
|
var formattedTime =
|
||||||
|
(hours < 10 ? "0" : "") + hours + ":" + (minutes < 10 ? "0" : "") + minutes;
|
||||||
|
var formattedDate =
|
||||||
|
(day < 10 ? "0" : "") +
|
||||||
|
day +
|
||||||
|
"-" +
|
||||||
|
(month < 10 ? "0" : "") +
|
||||||
|
month +
|
||||||
|
"-" +
|
||||||
|
year +
|
||||||
|
", " +
|
||||||
|
formattedTime;
|
||||||
|
|
||||||
|
return formattedDate;
|
||||||
|
}
|
||||||
|
export function convertDateFormatNoTime(dateString: any) {
|
||||||
|
var date = new Date(dateString);
|
||||||
|
|
||||||
|
var day = date.getDate();
|
||||||
|
var month = date.getMonth() + 1;
|
||||||
|
var year = date.getFullYear();
|
||||||
|
|
||||||
|
var formattedDate =
|
||||||
|
(day < 10 ? "0" : "") +
|
||||||
|
day +
|
||||||
|
"-" +
|
||||||
|
(month < 10 ? "0" : "") +
|
||||||
|
month +
|
||||||
|
"-" +
|
||||||
|
year;
|
||||||
|
|
||||||
|
return formattedDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertDateFormatNoTimeV2(dateString: string | Date) {
|
||||||
|
var date = new Date(dateString);
|
||||||
|
|
||||||
|
var day = date.getDate();
|
||||||
|
var month = date.getMonth() + 1;
|
||||||
|
var year = date.getFullYear();
|
||||||
|
|
||||||
|
var formattedDate =
|
||||||
|
year +
|
||||||
|
"-" +
|
||||||
|
(month < 10 ? "0" : "") +
|
||||||
|
month +
|
||||||
|
"-" +
|
||||||
|
(day < 10 ? "0" : "") +
|
||||||
|
day;
|
||||||
|
|
||||||
|
return formattedDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatTextToHtmlTag(text: string) {
|
||||||
|
if (text) {
|
||||||
|
const htmlText = text.replaceAll("\\n", "<br>").replaceAll(/"/g, "");
|
||||||
|
return { __html: htmlText };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function delay(ms: number) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function textEllipsis(
|
||||||
|
str: string,
|
||||||
|
maxLength: number,
|
||||||
|
{ side = "end", ellipsis = "..." } = {}
|
||||||
|
) {
|
||||||
|
if (str !== undefined && str?.length > maxLength) {
|
||||||
|
switch (side) {
|
||||||
|
case "start":
|
||||||
|
return ellipsis + str.slice(-(maxLength - ellipsis.length));
|
||||||
|
|
||||||
|
case "end":
|
||||||
|
default:
|
||||||
|
return str.slice(0, maxLength - ellipsis.length) + ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function htmlToString(str: string) {
|
||||||
|
if (str == undefined || str == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
str
|
||||||
|
.replaceAll(/<style[^>]*>.*<\/style>/gm, "")
|
||||||
|
// Remove script tags and content
|
||||||
|
.replaceAll(/<script[^>]*>.*<\/script>/gm, "")
|
||||||
|
// Replace  ,&ndash
|
||||||
|
.replaceAll(" ", "")
|
||||||
|
.replaceAll("–", "-")
|
||||||
|
// Replace quotation mark
|
||||||
|
.replaceAll("“", '"')
|
||||||
|
.replaceAll("”", '"')
|
||||||
|
// Remove all opening, closing and orphan HTML tags
|
||||||
|
.replaceAll(/<[^>]+>/gm, "")
|
||||||
|
// Remove leading spaces and repeated CR/LF
|
||||||
|
.replaceAll(/([\n\r]+ +)+/gm, "")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatMonthString(dateString: string) {
|
||||||
|
const months = [
|
||||||
|
"Januari",
|
||||||
|
"Februari",
|
||||||
|
"Maret",
|
||||||
|
"April",
|
||||||
|
"Mei",
|
||||||
|
"Juni",
|
||||||
|
"Juli",
|
||||||
|
"Agustus",
|
||||||
|
"September",
|
||||||
|
"Oktober",
|
||||||
|
"November",
|
||||||
|
"Desember",
|
||||||
|
];
|
||||||
|
|
||||||
|
const date = new Date(dateString); // Konversi string ke objek Date
|
||||||
|
const day = date.getDate(); // Ambil tanggal
|
||||||
|
const month = months[date.getMonth()]; // Ambil nama bulan
|
||||||
|
const year = date.getFullYear(); // Ambil tahun
|
||||||
|
|
||||||
|
return `${day} ${month} ${year}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setCookiesEncrypt(
|
||||||
|
param: string,
|
||||||
|
data: any,
|
||||||
|
options?: Cookies.CookieAttributes
|
||||||
|
) {
|
||||||
|
// Enkripsi data
|
||||||
|
const cookiesEncrypt = CryptoJS.AES.encrypt(
|
||||||
|
JSON.stringify(data),
|
||||||
|
`${param}_EncryptKey@humas`
|
||||||
|
).toString(); // Tambahkan .toString() di sini
|
||||||
|
|
||||||
|
// Simpan data terenkripsi di cookie
|
||||||
|
Cookies.set(param, cookiesEncrypt, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCookiesDecrypt(param: any) {
|
||||||
|
const cookiesEncrypt = Cookies.get(param);
|
||||||
|
try {
|
||||||
|
if (cookiesEncrypt != undefined) {
|
||||||
|
const output = CryptoJS.AES.decrypt(
|
||||||
|
cookiesEncrypt.toString(),
|
||||||
|
`${param}_EncryptKey@humas`
|
||||||
|
).toString(CryptoJS.enc.Utf8);
|
||||||
|
if (output.startsWith('"')) {
|
||||||
|
return output.slice(1, -1);
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
//console.log("Error", cookiesEncrypt);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue