From 87d2e8c830a2b21aebedca01bf4af763c613c220 Mon Sep 17 00:00:00 2001 From: Anang Yusman Date: Wed, 7 Jan 2026 16:06:07 +0800 Subject: [PATCH] update --- app/(admin)/admin/agent/create/page.tsx | 11 + app/(admin)/admin/agent/page.tsx | 41 ++ app/(admin)/admin/agent/update/[id]/page.tsx | 15 + app/(admin)/admin/banner/create/page.tsx | 9 + app/(admin)/admin/banner/detail/[id]/page.tsx | 22 + app/(admin)/admin/banner/edit/[id]/page.tsx | 22 + app/(admin)/admin/banner/page.tsx | 73 +++ app/(admin)/admin/galery/page.tsx | 48 ++ app/(admin)/admin/master-user/page.tsx | 2 +- app/(admin)/admin/product/create/page.tsx | 10 + app/(admin)/admin/product/page.tsx | 41 ++ app/(admin)/admin/product/update/page.tsx | 9 + app/(admin)/admin/promotion/create/page.tsx | 12 + app/(admin)/admin/promotion/page.tsx | 43 ++ components/dialog/agent-dialog.tsx | 98 +++ components/dialog/galery-detail-dialog.tsx | 146 +++++ components/dialog/galery-dialog.tsx | 197 ++++++ components/dialog/galery-update-dialog.tsx | 293 +++++++++ components/dialog/promo-dialog.tsx | 173 ++++++ components/form/agent/agent-form.tsx | 236 ++++++++ components/form/agent/update-agent-form.tsx | 285 +++++++++ components/form/banner-dialog.tsx | 191 ++++++ components/form/banner-edit-dialog.tsx | 226 +++++++ .../form/product/create-product-form.tsx | 293 +++++++++ .../form/product/update-product-form.tsx | 336 ++++++++++ .../form/promotion/create-promo-form.tsx | 123 ++++ .../landing-page/retracting-sidedar.tsx | 96 +-- .../main/dashboard/dashboard-container.tsx | 572 +++++++++++++----- components/table/agent-table.tsx | 563 +++++++++++++++++ components/table/article-table.tsx | 385 +++++++----- components/table/galery.tsx | 243 ++++++++ components/table/product-table.tsx | 562 +++++++++++++++++ components/table/promotion-table.tsx | 539 +++++++++++++++++ components/ui/form.tsx | 168 +++++ service/agent.ts | 41 ++ service/banner.ts | 51 ++ service/galery.ts | 72 +++ service/product.ts | 36 ++ service/promotion.ts | 39 ++ 39 files changed, 5991 insertions(+), 331 deletions(-) create mode 100644 app/(admin)/admin/agent/create/page.tsx create mode 100644 app/(admin)/admin/agent/page.tsx create mode 100644 app/(admin)/admin/agent/update/[id]/page.tsx create mode 100644 app/(admin)/admin/banner/create/page.tsx create mode 100644 app/(admin)/admin/banner/detail/[id]/page.tsx create mode 100644 app/(admin)/admin/banner/edit/[id]/page.tsx create mode 100644 app/(admin)/admin/banner/page.tsx create mode 100644 app/(admin)/admin/galery/page.tsx create mode 100644 app/(admin)/admin/product/create/page.tsx create mode 100644 app/(admin)/admin/product/page.tsx create mode 100644 app/(admin)/admin/product/update/page.tsx create mode 100644 app/(admin)/admin/promotion/create/page.tsx create mode 100644 app/(admin)/admin/promotion/page.tsx create mode 100644 components/dialog/agent-dialog.tsx create mode 100644 components/dialog/galery-detail-dialog.tsx create mode 100644 components/dialog/galery-dialog.tsx create mode 100644 components/dialog/galery-update-dialog.tsx create mode 100644 components/dialog/promo-dialog.tsx create mode 100644 components/form/agent/agent-form.tsx create mode 100644 components/form/agent/update-agent-form.tsx create mode 100644 components/form/banner-dialog.tsx create mode 100644 components/form/banner-edit-dialog.tsx create mode 100644 components/form/product/create-product-form.tsx create mode 100644 components/form/product/update-product-form.tsx create mode 100644 components/form/promotion/create-promo-form.tsx create mode 100644 components/table/agent-table.tsx create mode 100644 components/table/galery.tsx create mode 100644 components/table/product-table.tsx create mode 100644 components/table/promotion-table.tsx create mode 100644 components/ui/form.tsx create mode 100644 service/agent.ts create mode 100644 service/banner.ts create mode 100644 service/galery.ts create mode 100644 service/product.ts create mode 100644 service/promotion.ts diff --git a/app/(admin)/admin/agent/create/page.tsx b/app/(admin)/admin/agent/create/page.tsx new file mode 100644 index 0000000..df48b03 --- /dev/null +++ b/app/(admin)/admin/agent/create/page.tsx @@ -0,0 +1,11 @@ +import AddAgentForm from "@/components/form/agent/agent-form"; +import CreateArticleForm from "@/components/form/article/create-article-form"; +import AddProductForm from "@/components/form/product/create-product-form"; + +export default function CreateAgent() { + return ( +
+ +
+ ); +} diff --git a/app/(admin)/admin/agent/page.tsx b/app/(admin)/admin/agent/page.tsx new file mode 100644 index 0000000..382eba0 --- /dev/null +++ b/app/(admin)/admin/agent/page.tsx @@ -0,0 +1,41 @@ +"use client"; + +import { useState } from "react"; +import ArticleTable from "@/components/table/article-table"; +import { Button } from "@/components/ui/button"; +import { Plus } from "lucide-react"; +import { BannerDialog } from "@/components/form/banner-dialog"; +import Link from "next/link"; +import ProductTable from "@/components/table/product-table"; +import AgentTable from "@/components/table/agent-table"; + +export default function AgentPage() { + const [openDialog, setOpenDialog] = useState(false); + + const handleSubmitBanner = (data: any) => { + console.log("Banner Data:", data); + }; + + return ( +
+
+
+
+

Agent

+

Kelola Agent JAECOO

+
+ +
+ + + + +
+
+
+
+ ); +} diff --git a/app/(admin)/admin/agent/update/[id]/page.tsx b/app/(admin)/admin/agent/update/[id]/page.tsx new file mode 100644 index 0000000..41814da --- /dev/null +++ b/app/(admin)/admin/agent/update/[id]/page.tsx @@ -0,0 +1,15 @@ +import UpdateAgentForm from "@/components/form/agent/update-agent-form"; + +export default async function EditAgentPage({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = await params; + + return ( +
+ +
+ ); +} diff --git a/app/(admin)/admin/banner/create/page.tsx b/app/(admin)/admin/banner/create/page.tsx new file mode 100644 index 0000000..6e62d24 --- /dev/null +++ b/app/(admin)/admin/banner/create/page.tsx @@ -0,0 +1,9 @@ +import CreateArticleForm from "@/components/form/article/create-article-form"; + +export default function CreateArticle() { + return ( +
+ +
+ ); +} diff --git a/app/(admin)/admin/banner/detail/[id]/page.tsx b/app/(admin)/admin/banner/detail/[id]/page.tsx new file mode 100644 index 0000000..62d5a75 --- /dev/null +++ b/app/(admin)/admin/banner/detail/[id]/page.tsx @@ -0,0 +1,22 @@ +import EditArticleForm from "@/components/form/article/edit-article-form"; + +export default function DetailArticlePage() { + return ( +
+ {/*
+
+

Article

+

Article

+
+ + + + + +
*/} +
+ +
+
+ ); +} diff --git a/app/(admin)/admin/banner/edit/[id]/page.tsx b/app/(admin)/admin/banner/edit/[id]/page.tsx new file mode 100644 index 0000000..6c06089 --- /dev/null +++ b/app/(admin)/admin/banner/edit/[id]/page.tsx @@ -0,0 +1,22 @@ +import EditArticleForm from "@/components/form/article/edit-article-form"; + +export default function UpdateArticlePage() { + return ( +
+ {/*
+
+

Article

+

Article

+
+ + + + + +
*/} +
+ +
+
+ ); +} diff --git a/app/(admin)/admin/banner/page.tsx b/app/(admin)/admin/banner/page.tsx new file mode 100644 index 0000000..caa9b07 --- /dev/null +++ b/app/(admin)/admin/banner/page.tsx @@ -0,0 +1,73 @@ +"use client"; + +import { useState } from "react"; +import ArticleTable from "@/components/table/article-table"; +import { Button } from "@/components/ui/button"; +import { Plus } from "lucide-react"; +import { BannerDialog } from "@/components/form/banner-dialog"; +import { createBanner } from "@/service/banner"; +import router from "next/router"; +import { useRouter } from "next/navigation"; +import withReactContent from "sweetalert2-react-content"; +import Swal from "sweetalert2"; + +export default function BasicPage() { + const [openDialog, setOpenDialog] = useState(false); + const router = useRouter(); + const MySwal = withReactContent(Swal); + const [refreshKey, setRefreshKey] = useState(0); + const handleSubmitBanner = async (formData: FormData) => { + try { + const response = await createBanner(formData); + console.log("Banner created:", response); + } catch (error) { + console.error("Error creating banner:", error); + } + }; + + const successSubmit = () => { + MySwal.fire({ + title: "Sukses", + icon: "success", + confirmButtonColor: "#3085d6", + confirmButtonText: "OK", + }).then((result) => { + if (result.isConfirmed) { + setRefreshKey((prev) => prev + 1); // ⬅️ trigger refresh + } + }); + }; + + return ( +
+
+
+
+

Banner

+

Kelola gambar banner yang tampil di halaman Utama website

+
+ +
+ + + +
+
+
+ + {/* Dialog Tambah Banner */} + +
+ ); +} diff --git a/app/(admin)/admin/galery/page.tsx b/app/(admin)/admin/galery/page.tsx new file mode 100644 index 0000000..49f255a --- /dev/null +++ b/app/(admin)/admin/galery/page.tsx @@ -0,0 +1,48 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { Button } from "@/components/ui/button"; +import { Plus } from "lucide-react"; +import Galery from "@/components/table/galery"; +import { GaleriDialog } from "@/components/dialog/galery-dialog"; + +export default function GaleryPage() { + const [openDialog, setOpenDialog] = useState(false); + + const handleSubmitGaleri = () => { + console.log("Submit galeri..."); + setOpenDialog(false); + }; + + return ( +
+
+
+
+

Galeri

+

Kelola Galeri JAECOO

+
+ +
+ + + +
+
+
+ + setOpenDialog(false)} + onSubmit={handleSubmitGaleri} + /> +
+ ); +} diff --git a/app/(admin)/admin/master-user/page.tsx b/app/(admin)/admin/master-user/page.tsx index e534465..b051aff 100644 --- a/app/(admin)/admin/master-user/page.tsx +++ b/app/(admin)/admin/master-user/page.tsx @@ -13,7 +13,7 @@ export default function MasterUserPage() { + + + + + + + ); +} diff --git a/app/(admin)/admin/product/update/page.tsx b/app/(admin)/admin/product/update/page.tsx new file mode 100644 index 0000000..62f73c3 --- /dev/null +++ b/app/(admin)/admin/product/update/page.tsx @@ -0,0 +1,9 @@ +import UpdateProductForm from "@/components/form/product/update-product-form"; + +export default function CreateProduct() { + return ( +
+ +
+ ); +} diff --git a/app/(admin)/admin/promotion/create/page.tsx b/app/(admin)/admin/promotion/create/page.tsx new file mode 100644 index 0000000..f79502b --- /dev/null +++ b/app/(admin)/admin/promotion/create/page.tsx @@ -0,0 +1,12 @@ +import AddAgentForm from "@/components/form/agent/agent-form"; +import CreateArticleForm from "@/components/form/article/create-article-form"; +import AddProductForm from "@/components/form/product/create-product-form"; +import AddPromoForm from "@/components/form/promotion/create-promo-form"; + +export default function CreatePromo() { + return ( +
+ +
+ ); +} diff --git a/app/(admin)/admin/promotion/page.tsx b/app/(admin)/admin/promotion/page.tsx new file mode 100644 index 0000000..3c3d5b5 --- /dev/null +++ b/app/(admin)/admin/promotion/page.tsx @@ -0,0 +1,43 @@ +"use client"; + +import { useState } from "react"; +import ArticleTable from "@/components/table/article-table"; +import { Button } from "@/components/ui/button"; +import { Plus } from "lucide-react"; +import { BannerDialog } from "@/components/form/banner-dialog"; +import Link from "next/link"; +import ProductTable from "@/components/table/product-table"; +import AgentTable from "@/components/table/agent-table"; +import PromotionTable from "@/components/table/promotion-table"; + +export default function PromotionPage() { + const [openDialog, setOpenDialog] = useState(false); + + const handleSubmitBanner = (data: any) => { + console.log("Banner Data:", data); + // TODO: kirim data ke API di sini + }; + + return ( +
+
+
+
+

Promo

+

Kelola Promo JAECOO

+
+ +
+ + + + +
+
+
+
+ ); +} diff --git a/components/dialog/agent-dialog.tsx b/components/dialog/agent-dialog.tsx new file mode 100644 index 0000000..15ecc51 --- /dev/null +++ b/components/dialog/agent-dialog.tsx @@ -0,0 +1,98 @@ +"use client"; + +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import Image from "next/image"; +import { CheckCircle2 } from "lucide-react"; + +type AgentDetailProps = { + open: boolean; + onOpenChange: (open: boolean) => void; + data: { + name: string; + position: string; + phone: string; + status: "Aktif" | "Nonaktif"; + roles: string[]; + imageUrl: string; + } | null; +}; + +export default function AgentDetailDialog({ + open, + onOpenChange, + data, +}: AgentDetailProps) { + if (!data) return null; + + return ( + + +
+ + + Detail Agen + + + +
+ + {data.status} +
+
+ +
+
+ {data.name} +
+ +

{data.name}

+ +

{data.position}

+ +

{data.phone}

+ +
+

Jenis Agen

+ +
+ {data.roles.map((role) => ( +
+ + {role} +
+ ))} +
+
+
+ + {/* FOOTER */} +
+ +
+
+
+ ); +} diff --git a/components/dialog/galery-detail-dialog.tsx b/components/dialog/galery-detail-dialog.tsx new file mode 100644 index 0000000..41416ef --- /dev/null +++ b/components/dialog/galery-detail-dialog.tsx @@ -0,0 +1,146 @@ +"use client"; + +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; +import Image from "next/image"; +import { CheckCircle } from "lucide-react"; +import { useEffect, useState } from "react"; +import { getGaleryFileData } from "@/service/galery"; + +export function DialogDetailGaleri({ open, onClose, data }: any) { + const [images, setImages] = useState([]); + + const fetchImages = async () => { + try { + const res = await getGaleryFileData(data.id); + const allImages = res?.data?.data ?? []; + + const filteredImages = allImages.filter( + (img: any) => img.gallery_id === data.id + ); + + setImages(filteredImages); + } catch (e) { + console.error("Error fetch files:", e); + } + }; + + useEffect(() => { + if (open && data?.id) { + fetchImages(); + } + }, [open, data]); + + const openFile = (url: string) => { + window.open(url, "_blank"); + }; + + return ( + + + {/* Header */} +
+ Detail Galeri +
+ +
+ {/* Images List */} +
+

Daftar Gambar

+ +
+ {images.length === 0 && ( +

+ Tidak ada gambar. +

+ )} + + {images.map((img) => ( +
openFile(img.image_url)} + > + {img.title} + +
+ Lihat File +
+
+ ))} +
+
+ + {/* Title */} +

+ {data.title} +

+ + {/* Deskripsi */} +
+

Deskripsi

+

{data.description}

+
+ + {/* Tanggal Upload */} +
+

Tanggal Upload

+

+ {new Date(data.created_at).toLocaleDateString("id-ID")} +

+
+ + {/* Timeline */} +
+

+ Status Timeline +

+ +
+
+ +
+

Upload Berhasil

+

+ {new Date(data.created_at).toLocaleString("id-ID")} +

+
+
+ + {data.approved_at && ( +
+ +
+

Disetujui oleh Approver

+

+ {new Date(data.approved_at).toLocaleString("id-ID")} +

+
+
+ )} +
+
+
+ + + + +
+
+ ); +} diff --git a/components/dialog/galery-dialog.tsx b/components/dialog/galery-dialog.tsx new file mode 100644 index 0000000..f8c5df3 --- /dev/null +++ b/components/dialog/galery-dialog.tsx @@ -0,0 +1,197 @@ +"use client"; + +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Upload, X } from "lucide-react"; +import { useState, useRef, useEffect } from "react"; +import { createGalery, uploadGaleryFile } from "@/service/galery"; + +export function GaleriDialog({ open, onClose, onSubmit }: any) { + const [title, setTitle] = useState(""); + const [description, setDescription] = useState(""); + const [files, setFiles] = useState([]); + const [previews, setPreviews] = useState([]); + const fileRef = useRef(null); + + useEffect(() => { + if (!files || files.length === 0) { + setPreviews([]); + return; + } + + const objectUrls = files.map((file) => URL.createObjectURL(file)); + setPreviews(objectUrls); + + return () => { + objectUrls.forEach((url) => URL.revokeObjectURL(url)); + }; + }, [files]); + + const handleFileChange = (e: React.ChangeEvent) => { + const files = e.target.files; + if (!files) return; + setFiles((prev: File[]) => [...prev, ...Array.from(files)]); + }; + + const removeFile = (index: number) => { + setFiles((prev) => prev.filter((_, i) => i !== index)); + }; + + const handleSubmit = async () => { + try { + if (!title) return alert("Judul wajib diisi!"); + + const formData = new FormData(); + formData.append("title", title); + formData.append("description", description); + + const res = await createGalery(formData); + + const galleryId = res?.data?.data?.id; + if (!galleryId) { + alert("Galeri gagal dibuat"); + return; + } + + console.log("Galeri Created:", galleryId); + + for (const file of files) { + const fileForm = new FormData(); + fileForm.append("gallery_id", galleryId); + fileForm.append("title", title); + fileForm.append("file", file); + + const uploadRes = await uploadGaleryFile(galleryId, fileForm); + console.log("File Uploaded:", uploadRes?.data); + } + + onSubmit(); + + setTitle(""); + setDescription(""); + setFiles([]); + } catch (error) { + console.error("Submit failed:", error); + alert("Gagal mengupload galeri"); + } + }; + + return ( + + + {/* HEADER */} +
+ Tambah Galeri +
+ + {/* BODY */} +
+ {/* Judul */} +
+ + setTitle(e.target.value)} + /> +
+ + {/* Deskripsi */} +
+ + setDescription(e.target.value)} + /> +
+ + {/* Upload File */} +
+ + +
fileRef.current?.click()} + > + + +

+ Klik untuk upload atau drag & drop +

+

PNG, JPG (max 2MB)

+ + +
+ + {/* Preview Gambar */} + {previews.length > 0 && ( +
+ {previews.map((src, i) => ( +
+ {`preview-${i}`} + +
+ ))} +
+ )} +
+
+ + {/* FOOTER */} + + + + + +
+
+ ); +} diff --git a/components/dialog/galery-update-dialog.tsx b/components/dialog/galery-update-dialog.tsx new file mode 100644 index 0000000..87db128 --- /dev/null +++ b/components/dialog/galery-update-dialog.tsx @@ -0,0 +1,293 @@ +"use client"; + +import { useEffect, useState } from "react"; +import Image from "next/image"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; +import { X } from "lucide-react"; + +import { + getGaleryFileData, + updateGalery, + updateUploadGaleryFile, + deleteGaleryFile, +} from "@/service/galery"; +import { error, success } from "@/config/swal"; +import withReactContent from "sweetalert2-react-content"; +import Swal from "sweetalert2"; + +export function DialogUpdateGaleri({ open, onClose, data, onUpdated }: any) { + const [title, setTitle] = useState(""); + const MySwal = withReactContent(Swal); + const [description, setDescription] = useState(""); + const [oldFiles, setOldFiles] = useState([]); + const [newFiles, setNewFiles] = useState([]); + const [newFilePreviews, setNewFilePreviews] = useState([]); + const [loading, setLoading] = useState(false); + + const fetchOldFiles = async (galleryId: any) => { + try { + const res = await getGaleryFileData(galleryId); + const allImages = res?.data?.data ?? []; + const filtered = allImages.filter( + (img: any) => img.gallery_id === galleryId + ); + setOldFiles(filtered); + } catch (e) { + console.error("Error fetching files:", e); + } + }; + + useEffect(() => { + if (open && data?.id) { + setTitle(data.title ?? ""); + + setDescription(data.description ?? data.desc ?? ""); + fetchOldFiles(data.id); + + setNewFiles([]); + setNewFilePreviews([]); + } else if (!open) { + } + }, [open, data]); + + useEffect(() => { + if (!newFiles || newFiles.length === 0) { + setNewFilePreviews([]); + return; + } + + const urls = newFiles.map((f) => URL.createObjectURL(f)); + setNewFilePreviews(urls); + + return () => { + urls.forEach((u) => URL.revokeObjectURL(u)); + }; + }, [newFiles]); + + const handleUpload = (e: React.ChangeEvent) => { + const uploaded = Array.from(e.target.files || []) as File[]; + if (uploaded.length === 0) return; + setNewFiles((prev) => [...prev, ...uploaded]); + }; + + async function doDelete(id: any) { + const resDelete = await deleteGaleryFile(id); + if (resDelete?.error) { + error(resDelete.message); + return false; + } + success("Berhasil Hapus"); + return; + } + + const removeOldFile = (id: any) => { + MySwal.fire({ + title: "Hapus Data", + icon: "warning", + showCancelButton: true, + cancelButtonColor: "#3085d6", + confirmButtonColor: "#d33", + confirmButtonText: "Hapus", + }).then((result) => { + if (result.isConfirmed) { + doDelete(id); + } + }); + }; + + const removeNewFile = (index: number) => { + setNewFiles((prev) => prev.filter((_, i) => i !== index)); + setNewFilePreviews((prev) => prev.filter((_, i) => i !== index)); + }; + + const handleSubmit = async () => { + try { + if (!data?.id) { + alert("Gallery ID tidak tersedia."); + return; + } + + setLoading(true); + + const formMain = new FormData(); + formMain.append("title", title); + formMain.append("description", description); + formMain.append("old_files", JSON.stringify(oldFiles.map((f) => f.id))); + + const updateRes = await updateGalery(data.id, formMain); + console.log("updateGalery response:", updateRes); + + if (updateRes?.error) { + alert(updateRes.message || "Gagal update galeri"); + setLoading(false); + return; + } + + const galleryId = data.id; + const failedUploads: any[] = []; + + for (const file of newFiles) { + try { + const formFile = new FormData(); + formFile.append("gallery_id", String(galleryId)); + formFile.append("title", title); + formFile.append("is_active", "true"); + formFile.append("file", file); + + const uploadRes = await updateUploadGaleryFile(data.id, formFile); + + console.log("uploadRes:", uploadRes); + + if (uploadRes?.error) { + failedUploads.push({ + name: file.name, + error: uploadRes.message, + }); + } + } catch (err: any) { + failedUploads.push({ name: file.name, error: err }); + } + } + + setLoading(false); + + if (failedUploads.length > 0) { + let msg = failedUploads.map((f) => `${f.name}: ${f.error}`).join("\n"); + alert("Ada file gagal diupload:\n" + msg); + return; + } + + onClose(); + onUpdated && onUpdated(); + } catch (err: any) { + setLoading(false); + console.error("Update error:", err); + + if (err?.response?.data) { + alert("Server error: " + JSON.stringify(err.response.data)); + } else { + alert("Gagal menyimpan galeri"); + } + } + }; + + return ( + + + + Edit Galeri + + +
+ {/* TITLE */} +
+ + setTitle(e.target.value)} + /> +
+ + {/* DESCRIPTION */} +
+ +