This commit is contained in:
hanif salafi 2025-10-07 12:15:51 +07:00
commit 89da265615
5 changed files with 476 additions and 213 deletions

View File

@ -16,7 +16,7 @@ import Swal from "sweetalert2";
import { error } from "console"; import { error } from "console";
import { EyeFilledIcon, EyeSlashFilledIcon } from "../icons"; import { EyeFilledIcon, EyeSlashFilledIcon } from "../icons";
import { RadioGroup, RadioGroupItem } from "../ui/radio-group"; import { RadioGroup, RadioGroupItem } from "../ui/radio-group";
import { registerTenant } from "@/service/auth"; import { createUser, registerTenant } from "@/service/auth";
export default function SignUp() { export default function SignUp() {
const router = useRouter(); const router = useRouter();
@ -39,7 +39,9 @@ export default function SignUp() {
const [isResetPassword, setIsResetPassword] = useState(false); const [isResetPassword, setIsResetPassword] = useState(false);
const [checkUsernameValue, setCheckUsernameValue] = useState(""); const [checkUsernameValue, setCheckUsernameValue] = useState("");
const MySwal = withReactContent(Swal); const MySwal = withReactContent(Swal);
const [fullname, setFullname] = useState("");
const [userLevelId, setUserLevelId] = useState("");
const [userRoleId, setUserRoleId] = useState("");
const setValUsername = (e: any) => { const setValUsername = (e: any) => {
const uname = e.replaceAll(/[^\w.-]/g, ""); const uname = e.replaceAll(/[^\w.-]/g, "");
setUsername(uname.toLowerCase()); setUsername(uname.toLowerCase());
@ -81,16 +83,28 @@ export default function SignUp() {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [formErrors, setFormErrors] = useState<any>({}); const [formErrors, setFormErrors] = useState<any>({});
const handleSendOtp = (e: React.FormEvent) => { const handleSendOtp = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
// If role is tenant, handle tenant registration directly // Tenant → gunakan API registerTenant
if (role === "tenant") { if (role === "tenant") {
handleTenantRegistration(e); await handleTenantRegistration(e);
return; return;
} }
// For other roles, proceed with OTP flow // Umum dan Jurnalis → gunakan API createUser
if (role === "umum" || role === "jurnalis") {
await handleCreateUserUmum(e);
return;
}
// Kontributor (sementara ikut umum)
if (role === "kontributor") {
await handleCreateUserUmum(e);
return;
}
// Default (fallback ke OTP flow jika ada tambahan nanti)
setStep("otp"); setStep("otp");
}; };
@ -126,6 +140,76 @@ export default function SignUp() {
return password.length >= 8; return password.length >= 8;
}; };
const handleCreateUserUmum = async (e: React.FormEvent) => {
e.preventDefault();
if (!fullname.trim()) {
MySwal.fire("Peringatan", "Nama lengkap wajib diisi", "warning");
return;
}
if (!validateEmail(email)) {
MySwal.fire("Peringatan", "Email tidak valid", "warning");
return;
}
if (!validatePassword(password)) {
MySwal.fire("Peringatan", "Password minimal 8 karakter", "warning");
return;
}
if (!userLevelId || !userRoleId) {
MySwal.fire("Peringatan", "Level dan Role wajib diisi", "warning");
return;
}
// ✅ Generate username otomatis
const autoUsername =
fullname.trim().replace(/\s+/g, "-").toLowerCase() || email.split("@")[0];
const payload = {
address: "",
clientId: "78356d32-52fa-4dfc-b836-6cebf4e3eead",
dateOfBirth: "",
email,
fullName: fullname,
genderType: "",
identityGroup: "",
identityGroupNumber: "",
identityNumber: "",
identityType: "",
lastEducation: "",
password,
phoneNumber: "",
userLevelId: parseInt(userLevelId),
userRoleId: 3,
username: autoUsername,
workType: "",
};
console.log("📦 createUser payload:", payload);
try {
setIsLoading(true);
const res = await createUser(payload);
if (res?.error) {
MySwal.fire("Gagal", res?.message || "Gagal mendaftar", "error");
} else {
MySwal.fire({
title: "Berhasil!",
text: "Akun berhasil dibuat, Anda akan diarahkan ke halaman login.",
icon: "success",
showConfirmButton: false,
timer: 2000,
});
setTimeout(() => router.push("/auth"), 2000);
}
} catch (err) {
console.error("Error createUser:", err);
MySwal.fire("Error", "Terjadi kesalahan server.", "error");
} finally {
setIsLoading(false);
}
};
const validateTenantForm = () => { const validateTenantForm = () => {
const errors: any = {}; const errors: any = {};
@ -182,17 +266,17 @@ export default function SignUp() {
try { try {
const registrationData = { const registrationData = {
adminUser: { adminUser: {
address: "Jakarta", // Default address as per API requirement address: "Jakarta",
email: email, email: email,
fullname: `${firstName} ${lastName}`, fullname: `${firstName} ${lastName}`,
password: tenantPassword, password: tenantPassword,
phoneNumber: whatsapp, phoneNumber: whatsapp,
username: `${firstName}-${lastName}`, // Using firstName + lastName as username username: `${firstName}-${lastName}`,
}, },
client: { client: {
clientType: "sub_client", // Hardcoded as per API requirement clientType: "sub_client",
name: namaTenant, name: namaTenant,
parentClientId: "78356d32-52fa-4dfc-b836-6cebf4e3eead", // Hardcoded as per API requirement parentClientId: "78356d32-52fa-4dfc-b836-6cebf4e3eead",
}, },
}; };
@ -229,6 +313,17 @@ export default function SignUp() {
} }
}; };
// Generate username otomatis dari nama lengkap
React.useEffect(() => {
if (fullname.trim()) {
const generated = fullname
.toLowerCase()
.replace(/\s+/g, "-")
.replace(/[^a-z0-9_]/g, "-");
setUsername(generated);
}
}, [fullname]);
return ( return (
<div className="min-h-screen flex"> <div className="min-h-screen flex">
{/* Left Side - Logo Section */} {/* Left Side - Logo Section */}
@ -300,6 +395,86 @@ export default function SignUp() {
<Label htmlFor="tenant">Tenant</Label> <Label htmlFor="tenant">Tenant</Label>
</div> </div>
</RadioGroup> </RadioGroup>
{role === "umum" && (
<div className="mb-4 space-y-4">
{/* Nama Lengkap */}
<Input
type="text"
required
placeholder="Nama Lengkap"
value={fullname}
onChange={(e) => setFullname(e.target.value)}
/>
{/* Username (auto generated) */}
<Input
type="text"
placeholder="Username (otomatis dari nama)"
value={username}
readOnly
className="bg-gray-100 text-gray-700 cursor-not-allowed"
/>
{/* Email */}
<Input
type="email"
required
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
{/* Password */}
<div className="relative">
<Input
type={isVisible ? "text" : "password"}
required
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="pr-10"
/>
<button
type="button"
onClick={toggleVisibility}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-500 hover:text-gray-700"
>
{isVisible ? (
<EyeSlashFilledIcon className="h-4 w-4" />
) : (
<EyeFilledIcon className="h-4 w-4" />
)}
</button>
</div>
{/* Level dan Role */}
<select
required
className="w-full border border-gray-300 rounded-md p-2 text-sm"
value={userLevelId}
onChange={(e) => setUserLevelId(e.target.value)}
>
<option value="">Pilih Level Pengguna</option>
<option value="1">Mabes</option>
<option value="2">Polda</option>
<option value="3">Satker</option>
<option value="4">Polres</option>
<option value="5">Polsek</option>
</select>
<select
required
className="w-full border border-gray-300 rounded-md p-2 text-sm"
value={userRoleId}
onChange={(e) => setUserRoleId(e.target.value)}
>
<option value="">Pilih Role</option>
<option value="1">Admin</option>
<option value="2">Editor</option>
<option value="3">User</option>
</select>
</div>
)}
{/* Jurnalis: Select Keanggotaan */} {/* Jurnalis: Select Keanggotaan */}
{role === "jurnalis" && ( {role === "jurnalis" && (
@ -644,6 +819,12 @@ export default function SignUp() {
</div> </div>
) : role === "tenant" ? ( ) : role === "tenant" ? (
"Daftar Tenant" "Daftar Tenant"
) : role === "umum" ? (
"Daftar Umum"
) : role === "jurnalis" ? (
"Daftar Jurnalis"
) : role === "kontributor" ? (
"Daftar Kontributor"
) : ( ) : (
"Kirim OTP" "Kirim OTP"
)} )}

View File

@ -15,7 +15,6 @@ import {
useReactTable, useReactTable,
} from "@tanstack/react-table"; } from "@tanstack/react-table";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Table, Table,
TableBody, TableBody,
@ -46,7 +45,9 @@ import {
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { close, loading } from "@/config/swal"; import { close, loading } from "@/config/swal";
import { Link, useRouter } from "@/i18n/routing"; import { Link, useRouter } from "@/i18n/routing";
import { AdministrationUserList } from "@/service/service/management-user/management-user"; import { getAllUsers } from "@/service/management-user/management-user";
// ✅ Import service baru dari Swagger
const UserInternalTable = () => { const UserInternalTable = () => {
const router = useRouter(); const router = useRouter();
@ -69,11 +70,11 @@ const UserInternalTable = () => {
pageSize: Number(showData), pageSize: Number(showData),
}); });
const [search, setSearch] = React.useState(""); const [search, setSearch] = React.useState("");
const [categoryFilter, setCategoryFilter] = React.useState<number[]>([]); const [categoryFilter, setCategoryFilter] = React.useState<number[]>([]);
const [statusFilter, setStatusFilter] = React.useState<number[]>([]); const [statusFilter, setStatusFilter] = React.useState<number[]>([]);
const [page, setPage] = React.useState(1); const [page, setPage] = React.useState(1);
const [totalPage, setTotalPage] = React.useState(1); const [totalPage, setTotalPage] = React.useState(1);
const table = useReactTable({ const table = useReactTable({
data: dataTable, data: dataTable,
columns, columns,
@ -131,31 +132,41 @@ const UserInternalTable = () => {
}); });
}, [page, showData]); }, [page, showData]);
// === 🔥 Implementasi service getAllUsers dari Swagger ===
async function fetchData() { async function fetchData() {
try { try {
loading(); loading();
const res = await AdministrationUserList(
String(levelId), const res = await getAllUsers({
page - 1, count: 1,
search, limit: Number(showData),
showData, nextPage: page + 1,
"1", page: page,
statusFilter?.sort().join(",") previousPage: page - 1,
); sort: "desc",
const data = res?.data?.data; sortBy: "id",
const contentData = data?.content ?? []; totalPage: 10,
});
const data = res?.data;
const contentData = Array.isArray(data)
? data
: data?.content || data?.items || [];
// Tambahkan nomor urut
contentData.forEach((item: any, index: number) => { contentData.forEach((item: any, index: number) => {
item.no = (page - 1) * Number(showData) + index + 1; item.no = (page - 1) * Number(showData) + index + 1;
}); });
console.log("contentData : ", contentData); console.log("📦 contentData:", contentData);
setDataTable(contentData); setDataTable(contentData);
setTotalData(data?.totalElements ?? 0); setTotalData(data?.totalElements ?? contentData.length ?? 0);
setTotalPage(data?.totalPages ?? 0); setTotalPage(data?.totalPages ?? 1);
close(); close();
} catch (error) { } catch (error) {
console.error("Error fetching tasks:", error); console.error("❌ Error fetching users:", error);
close();
} }
} }
@ -336,6 +347,8 @@ const UserInternalTable = () => {
</Popover> </Popover>
</div> </div>
</div> </div>
{/* === TABLE === */}
<Table className="overflow-hidden"> <Table className="overflow-hidden">
<TableHeader> <TableHeader>
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
@ -377,6 +390,7 @@ const UserInternalTable = () => {
)} )}
</TableBody> </TableBody>
</Table> </Table>
<TablePagination <TablePagination
table={table} table={table}
totalData={totalData} totalData={totalData}

View File

@ -31,7 +31,7 @@ export type Group = {
}; };
export function getMenuList(pathname: string, t: any): Group[] { export function getMenuList(pathname: string, t: any): Group[] {
const roleId = getCookiesDecrypt("urie"); const roleId = Number(getCookiesDecrypt("urie"));
const levelNumber = getCookiesDecrypt("ulne"); const levelNumber = getCookiesDecrypt("ulne");
const userLevelId = getCookiesDecrypt("ulie"); const userLevelId = getCookiesDecrypt("ulie");
const userParentLevelId = getCookiesDecrypt("uplie"); const userParentLevelId = getCookiesDecrypt("uplie");
@ -55,49 +55,72 @@ export function getMenuList(pathname: string, t: any): Group[] {
}, },
], ],
}, },
{ ...(roleId === 2
groupLabel: "Content Management", ? [
id: "content", {
menus: [ groupLabel: "Content Management",
{ id: "content",
id: "content", menus: [
href: "/admin/content/image", {
label: "Content", id: "content",
active: pathname.includes("/content"), href: "/admin/content/image",
icon: "line-md:youtube", label: "Content",
submenus: [ active: pathname.includes("/content"),
{ icon: "line-md:youtube",
href: "/admin/content/image", submenus: [
label: "Image", {
active: pathname.includes("/content/image"), href: "/admin/content/image",
icon: "ic:outline-image", label: "Image",
children: [], active: pathname.includes("/content/image"),
}, icon: "ic:outline-image",
{ children: [],
href: "/admin/content/audio-visual", },
label: "Audio Visual", {
active: pathname.includes("/content/audio-visual"), href: "/admin/content/audio-visual",
icon: "line-md:youtube", label: "Audio Visual",
children: [], active: pathname.includes("/content/audio-visual"),
}, icon: "line-md:youtube",
{ children: [],
href: "/admin/content/document", },
label: "Document", {
active: pathname.includes("/content/document"), href: "/admin/content/document",
icon: "heroicons:document", label: "Document",
children: [], active: pathname.includes("/content/document"),
}, icon: "heroicons:document",
{ children: [],
href: "/admin/content/audio", },
label: "Audio", {
active: pathname.includes("/content/audio"), href: "/admin/content/audio",
icon: "heroicons:share", label: "Audio",
children: [], active: pathname.includes("/content/audio"),
}, icon: "heroicons:share",
], children: [],
}, },
], ],
}, },
],
},
]
: []),
...(roleId === 3
? [
{
groupLabel: "",
id: "management-user",
menus: [
{
id: "management-user-menu",
href: "/admin/management-user",
label: "Management User",
active: pathname.includes("/management-user"),
icon: "clarity:users-solid",
submenus: [],
},
],
},
]
: []),
// { // {
// groupLabel: "Master Data", // groupLabel: "Master Data",
// id: "master-data", // id: "master-data",
@ -120,127 +143,150 @@ export function getMenuList(pathname: string, t: any): Group[] {
// }, // },
// ], // ],
// }, // },
{ // {
groupLabel: "", // groupLabel: "",
id: "agenda-setting", // id: "agenda-setting",
menus: [ // menus: [
{ // {
id: "agenda-setting", // id: "agenda-setting",
href: "/admin/agenda-setting", // href: "/admin/agenda-setting",
label: t("agenda-setting"), // label: t("agenda-setting"),
active: pathname.includes("/agenda-setting"), // active: pathname.includes("/agenda-setting"),
icon: "iconoir:journal-page", // icon: "iconoir:journal-page",
submenus: [], // submenus: [],
}, // },
], // ],
}, // },
{ // {
groupLabel: "", // groupLabel: "",
id: "schedule", // id: "schedule",
menus: [ // menus: [
{ // {
id: "schedule", // id: "schedule",
href: "/admin/schedule", // href: "/admin/schedule",
label: t("schedule"), // label: t("schedule"),
active: pathname.includes("/schedule"), // active: pathname.includes("/schedule"),
icon: "uil:schedule", // icon: "uil:schedule",
submenus: [ // submenus: [
{ // {
href: "/admin/schedule/live-report", // href: "/admin/schedule/live-report",
label: t("live-report"), // label: t("live-report"),
active: pathname.includes("/schedule/live-report"), // active: pathname.includes("/schedule/live-report"),
icon: "heroicons:arrow-trending-up", // icon: "heroicons:arrow-trending-up",
children: [], // children: [],
}, // },
// { // // {
// href: "/contributor/schedule/press-conference", // // href: "/contributor/schedule/press-conference",
// label: t("press-conference"), // // label: t("press-conference"),
// active: pathname.includes("/schedule/press-conference"), // // active: pathname.includes("/schedule/press-conference"),
// icon: "heroicons:arrow-trending-up", // // icon: "heroicons:arrow-trending-up",
// children: [], // // children: [],
// }, // // },
// { // // {
// href: "/contributor/schedule/event", // // href: "/contributor/schedule/event",
// label: "event", // // label: "event",
// active: pathname.includes("/schedule/event"), // // active: pathname.includes("/schedule/event"),
// icon: "heroicons:shopping-cart", // // icon: "heroicons:shopping-cart",
// children: [], // // children: [],
// }, // // },
// { // // {
// href: "/contributor/schedule/press-release", // // href: "/contributor/schedule/press-release",
// label: t("press-release"), // // label: t("press-release"),
// active: pathname.includes("/schedule/press-release"), // // active: pathname.includes("/schedule/press-release"),
// icon: "heroicons:shopping-cart", // // icon: "heroicons:shopping-cart",
// children: [], // // children: [],
// }, // // },
// { // // {
// href: "/contributor/schedule/calendar-polri", // // href: "/contributor/schedule/calendar-polri",
// label: t("calendar-polri"), // // label: t("calendar-polri"),
// active: pathname.includes("/schedule/calendar-polri"), // // active: pathname.includes("/schedule/calendar-polri"),
// icon: "heroicons:arrow-trending-up", // // icon: "heroicons:arrow-trending-up",
// children: [], // // children: [],
// }, // // },
], // ],
}, // },
], // ],
}, // },
{ // {
groupLabel: "", // groupLabel: "",
id: "task", // id: "task",
menus: [ // menus: [
{ // {
id: "task", // id: "task",
href: "/admin/task", // href: "/admin/task",
label: t("task"), // label: t("task"),
active: pathname.includes("/task"), // active: pathname.includes("/task"),
icon: "fluent:clipboard-task-add-24-regular", // icon: "fluent:clipboard-task-add-24-regular",
submenus: [], // submenus: [],
}, // },
], // ],
}, // },
{ // {
groupLabel: "", // groupLabel: "",
id: "communication", // id: "communication",
menus: [ // menus: [
{ // {
id: "communication", // id: "communication",
href: "/admin/shared/communication", // href: "/admin/shared/communication",
label: t("communication"), // label: t("communication"),
active: pathname.includes("/communication"), // active: pathname.includes("/communication"),
icon: "token:chat", // icon: "token:chat",
submenus: [], // submenus: [],
}, // },
], // ],
}, // },
{ ...(roleId === 3
groupLabel: "Settings", ? [
id: "settings", {
menus: [ groupLabel: "",
{ id: "tenant",
id: "settings", menus: [
href: "/admin/settings", {
label: "Settings", id: "tenant",
active: pathname.includes("/settings"), href: "/admin/settings/tenant",
icon: "heroicons:cog-6-tooth", label: "Tenant",
submenus: [ active: pathname.includes("/tenant"),
{ icon: "token:chat",
href: "/admin/categories", submenus: [],
label: "Categories", },
active: pathname.includes("/categories"), ],
icon: "ic:outline-image", },
children: [], ]
}, : []),
{ ...(roleId === 2
href: "/admin/settings/tenant", ? [
label: "Tenant", {
active: pathname.includes("/tenant"), groupLabel: "Settings",
icon: "ic:outline-image", id: "settings",
children: [], menus: [
}, {
], id: "settings",
}, href: "/admin/settings",
], label: "Settings",
}, active: pathname.includes("/settings"),
icon: "heroicons:cog-6-tooth",
submenus: [
{
href: "/admin/categories",
label: "Categories",
active: pathname.includes("/categories"),
icon: "ic:outline-image",
children: [],
},
// kalau nanti Tenant mau dimunculkan lagi:
// {
// href: "/admin/settings/tenant",
// label: "Tenant",
// active: pathname.includes("/tenant"),
// icon: "ic:outline-image",
// children: [],
// },
],
},
],
},
]
: []),
]; ];
return menusSelected; return menusSelected;
} }

View File

@ -7,25 +7,25 @@ import {
postAPIWithJson, postAPIWithJson,
} from "./http-config/http-interceptor-service"; } from "./http-config/http-interceptor-service";
export interface CreateUserPayload { // export interface CreateUserPayload {
address: string; // address: string;
clientId: string; // clientId: string;
dateOfBirth: string; // dateOfBirth: string;
email: string; // email: string;
fullName: string; // fullName: string;
genderType: string; // genderType: string;
identityGroup: string; // identityGroup: string;
identityGroupNumber: string; // identityGroupNumber: string;
identityNumber: string; // identityNumber: string;
identityType: string; // identityType: string;
lastEducation: string; // lastEducation: string;
password: string; // password: string;
phoneNumber: string; // phoneNumber: string;
userLevelId: number; // userLevelId: number;
userRoleId: number; // userRoleId: number;
username: string; // username: string;
workType: string; // workType: string;
} // }
export interface RequestOtpPayload { export interface RequestOtpPayload {
email: string; email: string;
@ -201,7 +201,7 @@ export async function getDataPersonil(nrp: any) {
return httpGetInterceptor(url); return httpGetInterceptor(url);
} }
export async function createUser(data: CreateUserPayload) { export async function createUser(data: any) {
const url = "users"; const url = "users";
return httpPost(url, data); return httpPost(url, data);
} }
@ -214,4 +214,3 @@ export async function registerTenant(data: any) {
const url = "clients/with-user"; const url = "clients/with-user";
return httpPost(url, data); return httpPost(url, data);
} }

View File

@ -21,6 +21,29 @@ export async function AdministrationUserList(
return httpGetInterceptor(url); return httpGetInterceptor(url);
} }
export async function getAllUsers({
count = 1,
limit = 10,
nextPage = 1,
page = 1,
previousPage = 1,
sort = "desc",
sortBy = "id",
totalPage = 10,
}: {
count?: number;
limit?: number;
nextPage?: number;
page?: number;
previousPage?: number;
sort?: string;
sortBy?: string;
totalPage?: number;
}) {
const url = `users?count=${count}&limit=${limit}&nextPage=${nextPage}&page=${page}&previousPage=${previousPage}&sort=${sort}&sortBy=${sortBy}&totalPage=${totalPage}`;
return httpGetInterceptor(url);
}
export async function AdministrationLevelList() { export async function AdministrationLevelList() {
const url = "users/user-levels/list"; const url = "users/user-levels/list";
return httpGetInterceptor(url); return httpGetInterceptor(url);