feat: update approval for all module

This commit is contained in:
hanif salafi 2026-01-20 08:04:42 +07:00
parent 9ee2433a78
commit 98bace5dcc
14 changed files with 386 additions and 32 deletions

View File

@ -47,9 +47,9 @@ import {
} from "@/components/ui/table";
import CustomPagination from "../layout/custom-pagination";
import { EditBannerDialog } from "../form/banner-edit-dialog";
import { Eye } from "lucide-react";
import { Eye, CheckCheck } from "lucide-react";
import AgentDetailDialog from "../dialog/agent-dialog";
import { deleteAgent, getAgentData } from "@/service/agent";
import { deleteAgent, getAgentData, approveAgent } from "@/service/agent";
const columns = [
{ name: "No", uid: "no" },
@ -94,6 +94,13 @@ export default function AgentTable() {
startDate: null,
endDate: null,
});
const [userLevelId, setUserLevelId] = useState<string | null>(null);
// 🔹 Ambil userlevelId dari cookies
useEffect(() => {
const ulne = Cookies.get("ulne"); // contoh: "3"
setUserLevelId(ulne ?? null);
}, []);
useEffect(() => {
initState();
@ -180,6 +187,21 @@ export default function AgentTable() {
setOpenPreview(true);
};
const handleApproveAgent = async (id: number) => {
loading();
const res = await approveAgent(id);
if (res?.error) {
error(res.message || "Gagal menyetujui agent");
close();
return;
}
close();
success("Agent berhasil disetujui");
initState(); // refresh table
};
const copyUrlArticle = async (id: number, slug: string) => {
const url =
`${window.location.protocol}//${window.location.host}` +
@ -386,9 +408,19 @@ export default function AgentTable() {
{/* STATUS */}
<TableCell className="text-center">
<span className="bg-yellow-100 text-yellow-800 text-xs font-medium px-3 py-1 rounded-full">
Menunggu
</span>
{item.status_id === 1 ? (
<span className="bg-yellow-100 text-yellow-700 text-xs px-3 py-1 rounded-full font-medium">
Menunggu
</span>
) : item.status_id === 2 ? (
<span className="bg-green-100 text-green-700 text-xs px-3 py-1 rounded-full font-medium">
Disetujui
</span>
) : (
<span className="bg-gray-100 text-gray-600 text-xs px-3 py-1 rounded-full font-medium">
{item.status_id || "Tidak Diketahui"}
</span>
)}
</TableCell>
{/* AKSI */}
@ -426,6 +458,18 @@ export default function AgentTable() {
</Button>
</Link>
{/* Tombol Approve - hanya untuk admin dan status pending */}
{userLevelId === "1" && item.status_id === 1 && (
<Button
variant="ghost"
size="sm"
onClick={() => handleApproveAgent(item.id)}
className="text-green-600 hover:bg-transparent hover:underline p-0"
>
<CheckCheck className="w-4 h-4 mr-1" />
Approve
</Button>
)}
{/* Tombol Hapus */}
<Button
variant="ghost"

View File

@ -46,7 +46,7 @@ import {
} from "@/components/ui/table";
import CustomPagination from "../layout/custom-pagination";
import { EditBannerDialog } from "../form/banner-edit-dialog";
import { deleteBanner, getBannerData, updateBanner } from "@/service/banner";
import { deleteBanner, getBannerData, updateBanner, approveBanner } from "@/service/banner";
import { Check, CheckCheck, Clock, Eye, X } from "lucide-react";
import { useRouter } from "next/navigation";
@ -239,6 +239,21 @@ export default function ArticleTable() {
initState(); // refresh table
};
const handleApproveBanner = async (id: number) => {
loading();
const res = await approveBanner(id);
if (res?.error) {
error(res.message || "Gagal menyetujui banner");
close();
return;
}
close();
success("Banner berhasil disetujui");
initState(); // refresh table
};
const handleReject = async () => {
if (!viewBanner) return;
@ -463,21 +478,17 @@ export default function ArticleTable() {
{/* STATUS */}
<TableCell className="text-center">
{item.status === "1" ? (
{item.status_id === 1 ? (
<span className="bg-yellow-100 text-yellow-700 text-xs px-3 py-1 rounded-full font-medium">
Menunggu
</span>
) : item.status === "2" ? (
) : item.status_id === 2 ? (
<span className="bg-green-100 text-green-700 text-xs px-3 py-1 rounded-full font-medium">
Disetujui
</span>
) : item.status === "3" ? (
<span className="bg-red-100 text-red-700 text-xs px-3 py-1 rounded-full font-medium">
Canceled
</span>
) : (
<span className="bg-gray-100 text-gray-600 text-xs px-3 py-1 rounded-full font-medium">
Tidak Diketahui
{item.status_id || "Tidak Diketahui"}
</span>
)}
</TableCell>
@ -505,6 +516,17 @@ export default function ArticleTable() {
Edit
</Button>
)}
{userLevelId === "1" && item.status_id === 1 && (
<Button
variant="ghost"
size="sm"
className="text-green-600 hover:bg-transparent hover:underline p-0"
onClick={() => handleApproveBanner(item.id)}
>
<CheckCheck className="w-4 h-4 mr-1" />
Approve
</Button>
)}
<Button
variant="ghost"
size="sm"

View File

@ -2,22 +2,25 @@
import { useEffect, useState } from "react";
import Image from "next/image";
import { Eye, Pencil, Trash2, Calendar, MapPin } from "lucide-react";
import { Eye, Pencil, Trash2, Calendar, MapPin, CheckCheck } from "lucide-react";
import {
deleteGalery,
getGaleryById,
getGaleryData,
getGaleryFileData,
approveGalery,
} from "@/service/galery";
import { DialogDetailGaleri } from "../dialog/galery-detail-dialog";
import { DialogUpdateGaleri } from "../dialog/galery-update-dialog";
import { error, success } from "@/config/swal";
import { error, success, loading, close } from "@/config/swal";
import withReactContent from "sweetalert2-react-content";
import Swal from "sweetalert2";
import Cookies from "js-cookie";
export default function Galery() {
const MySwal = withReactContent(Swal);
const [data, setData] = useState<any[]>([]);
const [userLevelId, setUserLevelId] = useState<string | null>(null);
const [page, setPage] = useState(1);
const [showData, setShowData] = useState("10");
@ -70,6 +73,12 @@ export default function Galery() {
}
};
// 🔹 Ambil userlevelId dari cookies
useEffect(() => {
const ulne = Cookies.get("ulne"); // contoh: "3"
setUserLevelId(ulne ?? null);
}, []);
useEffect(() => {
fetchData();
}, [page, showData, search]);
@ -120,6 +129,21 @@ export default function Galery() {
});
};
const handleApproveGalery = async (id: number) => {
loading();
const res = await approveGalery(id);
if (res?.error) {
error(res.message || "Gagal menyetujui galeri");
close();
return;
}
close();
success("Galeri berhasil disetujui");
fetchData(); // refresh table
};
return (
<div className="mt-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
@ -147,12 +171,18 @@ export default function Galery() {
<span
className={`absolute top-3 left-3 text-xs px-3 py-1 rounded-full font-medium
${
item.is_active
item.status_id === 2
? "bg-green-100 text-green-700"
: "bg-red-100 text-red-600"
: item.status_id === 1
? "bg-yellow-100 text-yellow-700"
: "bg-gray-100 text-gray-600"
}`}
>
{item.is_active ? "Aktif" : "Tidak Aktif"}
{item.status_id === 2
? "Disetujui"
: item.status_id === 1
? "Menunggu"
: "Tidak Diketahui"}
</span>
</div>
@ -189,6 +219,16 @@ export default function Galery() {
<Pencil className="h-4 w-4" /> Edit
</button>
{/* Tombol Approve - hanya untuk admin dan status pending */}
{userLevelId === "1" && item.status_id === 1 && (
<button
className="flex items-center gap-1 text-green-600 hover:text-green-700 transition"
onClick={() => handleApproveGalery(item.id)}
>
<CheckCheck className="h-4 w-4" /> Approve
</button>
)}
<button
className="flex items-center gap-1 text-red-600 hover:text-red-700 transition"
onClick={() => handleDelete(item.id)}

View File

@ -47,7 +47,7 @@ import {
} from "@/components/ui/table";
import CustomPagination from "../layout/custom-pagination";
import { EditBannerDialog } from "../form/banner-edit-dialog";
import { deleteProduct, getProductPagination } from "@/service/product";
import { deleteProduct, getProductPagination, approveProduct } from "@/service/product";
import { CheckCheck, Eye } from "lucide-react";
import { useRouter } from "next/navigation";
@ -194,6 +194,21 @@ export default function ProductTable() {
setOpenPreview(true);
};
const handleApproveProduct = async (id: number) => {
loading();
const res = await approveProduct(id);
if (res?.error) {
error(res.message || "Gagal menyetujui product");
close();
return;
}
close();
success("Product berhasil disetujui");
initState(); // refresh table
};
const copyUrlArticle = async (id: number, slug: string) => {
const url =
`${window.location.protocol}//${window.location.host}` +
@ -425,13 +440,17 @@ export default function ProductTable() {
{/* STATUS */}
<TableCell className="text-center">
{item.status === "Disetujui" ? (
<span className="bg-green-100 text-green-700 text-xs font-medium px-3 py-1 rounded-full">
{item.status_id === 1 ? (
<span className="bg-yellow-100 text-yellow-700 text-xs px-3 py-1 rounded-full font-medium">
Menunggu
</span>
) : item.status_id === 2 ? (
<span className="bg-green-100 text-green-700 text-xs px-3 py-1 rounded-full font-medium">
Disetujui
</span>
) : (
<span className="bg-yellow-100 text-yellow-800 text-xs font-medium px-3 py-1 rounded-full">
Menunggu
<span className="bg-gray-100 text-gray-600 text-xs px-3 py-1 rounded-full font-medium">
{item.status_id || "Tidak Diketahui"}
</span>
)}
</TableCell>
@ -460,6 +479,17 @@ export default function ProductTable() {
</Button>
</Link>
)}
{userLevelId === "1" && item.status_id === 1 && (
<Button
variant="ghost"
size="sm"
onClick={() => handleApproveProduct(item.id)}
className="text-green-600 hover:bg-transparent hover:underline p-0"
>
<CheckCheck className="w-4 h-4 mr-1" />
Approve
</Button>
)}
<Button
variant="ghost"
size="sm"

View File

@ -47,10 +47,10 @@ import {
} from "@/components/ui/table";
import CustomPagination from "../layout/custom-pagination";
import { EditBannerDialog } from "../form/banner-edit-dialog";
import { Eye, Trash2 } from "lucide-react";
import { Eye, Trash2, CheckCheck } from "lucide-react";
import AgentDetailDialog from "../dialog/agent-dialog";
import PromoDetailDialog from "../dialog/promo-dialog";
import { deletePromotion, getPromotionPagination } from "@/service/promotion";
import { deletePromotion, getPromotionPagination, approvePromotion } from "@/service/promotion";
const columns = [
{ name: "No", uid: "no" },
@ -96,6 +96,13 @@ export default function PromotionTable() {
startDate: null,
endDate: null,
});
const [userLevelId, setUserLevelId] = useState<string | null>(null);
// 🔹 Ambil userlevelId dari cookies
useEffect(() => {
const ulne = Cookies.get("ulne"); // contoh: "3"
setUserLevelId(ulne ?? null);
}, []);
useEffect(() => {
initState();
@ -182,6 +189,21 @@ export default function PromotionTable() {
setOpenPreview(true);
};
const handleApprovePromotion = async (id: number) => {
loading();
const res = await approvePromotion(id);
if (res?.error) {
error(res.message || "Gagal menyetujui promotion");
close();
return;
}
close();
success("Promotion berhasil disetujui");
initState(); // refresh table
};
const copyUrlArticle = async (id: number, slug: string) => {
const url =
`${window.location.protocol}//${window.location.host}` +
@ -378,9 +400,19 @@ export default function PromotionTable() {
{/* STATUS */}
<TableCell className="text-center">
<span className="bg-yellow-100 text-yellow-800 text-xs font-medium px-3 py-1 rounded-full">
Menunggu
</span>
{item.status_id === 1 ? (
<span className="bg-yellow-100 text-yellow-700 text-xs px-3 py-1 rounded-full font-medium">
Menunggu
</span>
) : item.status_id === 2 ? (
<span className="bg-green-100 text-green-700 text-xs px-3 py-1 rounded-full font-medium">
Disetujui
</span>
) : (
<span className="bg-gray-100 text-gray-600 text-xs px-3 py-1 rounded-full font-medium">
{item.status_id || "Tidak Diketahui"}
</span>
)}
</TableCell>
{/* AKSI */}
@ -400,6 +432,18 @@ export default function PromotionTable() {
</Button>
{/* Tombol Edit */}
{/* Tombol Approve - hanya untuk admin dan status pending */}
{userLevelId === "1" && item.status_id === 1 && (
<Button
variant="ghost"
size="sm"
onClick={() => handleApprovePromotion(item.id)}
className="text-green-600 hover:bg-transparent hover:underline p-0"
>
<CheckCheck className="w-4 h-4 mr-1" />
Approve
</Button>
)}
{/* Tombol Hapus */}
<Button
variant="ghost"

61
service/ENV_SETUP.md Normal file
View File

@ -0,0 +1,61 @@
# Environment Setup untuk API Services
## Cara Setup Environment Variables
Buat file `.env.local` di root project dengan konfigurasi berikut:
```env
# API Endpoints
NEXT_PUBLIC_API_BASE_URL=https://jaecookelapagading.com/api
NEXT_PUBLIC_LOCAL_API_URL=http://localhost:8800
NEXT_PUBLIC_CLIENT_KEY=bb65b1ad-e954-4a1a-b4d0-74df5bb0b640
```
## Penjelasan
- `NEXT_PUBLIC_API_BASE_URL`: URL base untuk API (production atau development)
- `NEXT_PUBLIC_LOCAL_API_URL`: URL untuk API lokal (localhost:8800)
- `NEXT_PUBLIC_CLIENT_KEY`: Client key untuk authentikasi API
## Cara Menggunakan
### 1. Untuk memanggil localhost:8800
Ubah `NEXT_PUBLIC_API_BASE_URL` di `.env.local`:
```env
NEXT_PUBLIC_API_BASE_URL=http://localhost:8800
```
### 2. Untuk memanggil API production
```env
NEXT_PUBLIC_API_BASE_URL=https://jaecookelapagading.com/api
```
### 3. Menggunakan service
```typescript
// Tanpa authentication
import { getDataFromLocal, postDataToLocal } from '@/service/local-api-service';
const data = await getDataFromLocal('/endpoint');
// Dengan authentication (menggunakan Bearer token dari cookies)
import { getDataWithAuth, postDataWithAuth } from '@/service/local-api-service';
const data = await getDataWithAuth('/endpoint');
```
## File yang Sudah Diupdate
- `service/http-config/axios-base-instance.ts` - Membaca dari env variable
- `service/http-config/axios-interceptor-instance.ts` - Membaca dari env variable
- `service/local-api-service.ts` - Service helper untuk memanggil API
## Catatan
- Pastikan file `.env.local` tidak di-commit ke repository
- File `.env.local` sudah ada di `.gitignore`
- Restart development server setelah mengubah environment variables

View File

@ -39,3 +39,10 @@ export async function deleteAgent(id: string) {
};
return await httpDeleteInterceptor(`sales-agents/${id}`, headers);
}
export async function approveAgent(id: string | number) {
const headers = {
"content-type": "application/json",
};
return await httpPutInterceptor(`/sales-agents/${id}/approve`, {}, headers);
}

View File

@ -49,3 +49,10 @@ export async function deleteBanner(id: string) {
};
return await httpDeleteInterceptor(`banners/${id}`, headers);
}
export async function approveBanner(id: string | number) {
const headers = {
"content-type": "application/json",
};
return await httpPutInterceptor(`/banners/${id}/approve`, {}, headers);
}

View File

@ -70,3 +70,10 @@ export async function deleteGaleryFile(id: any) {
};
return await httpDeleteInterceptor(`gallery-files/${id}`, headers);
}
export async function approveGalery(id: string | number) {
const headers = {
"content-type": "application/json",
};
return await httpPutInterceptor(`/galleries/${id}/approve`, {}, headers);
}

View File

@ -1,12 +1,14 @@
import axios from "axios";
const baseURL = "https://jaecookelapagading.com/api";
// Mengambil base URL dari environment variable, default ke API production
const baseURL = process.env.NEXT_PUBLIC_API_BASE_URL || "https://jaecookelapagading.com/api";
const clientKey = process.env.NEXT_PUBLIC_CLIENT_KEY || "bb65b1ad-e954-4a1a-b4d0-74df5bb0b640";
const axiosBaseInstance = axios.create({
baseURL,
headers: {
"Content-Type": "application/json",
"X-Client-Key": "bb65b1ad-e954-4a1a-b4d0-74df5bb0b640",
"X-Client-Key": clientKey,
},
});

View File

@ -2,7 +2,9 @@ import axios from "axios";
import { postSignIn } from "../master-user";
import Cookies from "js-cookie";
const baseURL = "https://jaecookelapagading.com/api";
// Mengambil base URL dari environment variable, default ke API production
const baseURL = process.env.NEXT_PUBLIC_API_BASE_URL || "https://jaecookelapagading.com/api";
const clientKey = process.env.NEXT_PUBLIC_CLIENT_KEY || "bb65b1ad-e954-4a1a-b4d0-74df5bb0b640";
const refreshToken = Cookies.get("refresh_token");
@ -10,7 +12,7 @@ const axiosInterceptorInstance = axios.create({
baseURL,
headers: {
"Content-Type": "application/json",
"X-Client-Key": "bb65b1ad-e954-4a1a-b4d0-74df5bb0b640",
"X-Client-Key": clientKey,
},
withCredentials: true,
});

View File

@ -0,0 +1,74 @@
/**
* Service untuk memanggil API lokal (localhost:8800)
* Menggunakan endpoint dari .env
*
* Contoh penggunaan:
* - Set NEXT_PUBLIC_API_BASE_URL=http://localhost:8800 di file .env.local
* - Atau gunakan NEXT_PUBLIC_LOCAL_API_URL jika ingin endpoint terpisah
*/
import {
httpGet,
httpPost,
} from "./http-config/http-base-services";
import {
httpGetInterceptor,
httpPostInterceptor,
httpPutInterceptor,
httpDeleteInterceptor,
} from "./http-config/http-interceptor-services";
// Service untuk endpoint yang tidak memerlukan authentication
// Menggunakan axios-base-instance.ts
export async function getDataFromLocal(endpoint: string, headers?: any) {
return await httpGet(endpoint, headers);
}
export async function postDataToLocal(endpoint: string, data: any, headers?: any) {
return await httpPost(endpoint, data, headers);
}
// Service untuk endpoint yang memerlukan authentication
// Menggunakan axios-interceptor-instance.ts dengan Bearer token
export async function getDataWithAuth(endpoint: string, headers?: any) {
return await httpGetInterceptor(endpoint, headers);
}
export async function postDataWithAuth(endpoint: string, data: any, headers?: any) {
return await httpPostInterceptor(endpoint, data, headers);
}
export async function updateDataWithAuth(endpoint: string, data: any, headers?: any) {
return await httpPutInterceptor(endpoint, data, headers);
}
export async function deleteDataWithAuth(endpoint: string, headers?: any) {
return await httpDeleteInterceptor(endpoint, headers);
}
// Contoh implementasi spesifik untuk endpoint /api/users
// Gunakan service dengan auth jika endpoint memerlukan token
export async function getUsers() {
return await httpGetInterceptor("/users");
}
export async function getUserById(id: string | number) {
return await httpGetInterceptor(`/users/${id}`);
}
export async function createUser(data: any) {
return await httpPostInterceptor("/users", data);
}
export async function updateUser(id: string | number, data: any) {
return await httpPutInterceptor(`/users/${id}`, data);
}
export async function deleteUser(id: string | number) {
return await httpDeleteInterceptor(`/users/${id}`);
}

View File

@ -42,3 +42,10 @@ export async function deleteProduct(id: string) {
};
return await httpDeleteInterceptor(`products/${id}`, headers);
}
export async function approveProduct(id: string | number) {
const headers = {
"content-type": "application/json",
};
return await httpPutInterceptor(`/products/${id}/approve`, {}, headers);
}

View File

@ -37,3 +37,10 @@ export async function deletePromotion(id: string) {
};
return await httpDeleteInterceptor(`promotions/${id}`, headers);
}
export async function approvePromotion(id: string | number) {
const headers = {
"content-type": "application/json",
};
return await httpPutInterceptor(`/promotions/${id}/approve`, {}, headers);
}