fix: fixing all eror in excel

This commit is contained in:
Sabda Yagra 2025-11-23 22:37:29 +07:00
parent dd94afc81d
commit 8f494739c6
15 changed files with 1561 additions and 711 deletions

View File

@ -18,7 +18,7 @@ import { useRouter } from "next/navigation";
import { deleteUser } from "@/service/management-user/management-user"; import { deleteUser } from "@/service/management-user/management-user";
import { stringify } from "querystring"; import { stringify } from "querystring";
const columns: ColumnDef<any>[] = [ const getColumns = ({ onRefresh }: { onRefresh: () => void }): ColumnDef<any>[] => [
{ {
accessorKey: "no", accessorKey: "no",
header: "No", header: "No",
@ -30,11 +30,13 @@ const columns: ColumnDef<any>[] = [
header: "Nama", header: "Nama",
cell: ({ row }) => <span>{row.getValue("fullname")}</span>, cell: ({ row }) => <span>{row.getValue("fullname")}</span>,
}, },
{ {
accessorKey: "address", accessorKey: "address",
header: "Wilayah", header: "Wilayah",
cell: ({ row }) => <span>MABES</span>, cell: () => <span>MABES</span>,
}, },
{ {
accessorKey: "userRolePlacements", accessorKey: "userRolePlacements",
header: "Posisi", header: "Posisi",
@ -52,6 +54,7 @@ const columns: ColumnDef<any>[] = [
return <span>{posisi}</span>; return <span>{posisi}</span>;
}, },
}, },
{ {
accessorKey: "role.name", accessorKey: "role.name",
header: "Bidang Keahlian", header: "Bidang Keahlian",
@ -81,29 +84,24 @@ const columns: ColumnDef<any>[] = [
{ {
id: "actions", id: "actions",
accessorKey: "action",
header: "Actions", header: "Actions",
enableHiding: false,
cell: ({ row }) => { cell: ({ row }) => {
const { toast } = useToast(); const { toast } = useToast();
const MySwal = withReactContent(Swal); const MySwal = withReactContent(Swal);
const router = useRouter();
const doDelete = async (id: number) => { const doDelete = async (id: number) => {
// Tampilkan loading
Swal.fire({ Swal.fire({
title: "Menghapus user...", title: "Menghapus user...",
text: "Mohon tunggu sebentar", text: "Mohon tunggu",
allowOutsideClick: false, allowOutsideClick: false,
didOpen: () => { didOpen: () => Swal.showLoading(),
Swal.showLoading();
},
}); });
const response = await deleteUser(id); const response = await deleteUser(id);
if (response?.error) { Swal.close();
Swal.close();
if (response?.error) {
toast({ toast({
title: stringify(response?.message), title: stringify(response?.message),
variant: "destructive", variant: "destructive",
@ -111,88 +109,52 @@ const columns: ColumnDef<any>[] = [
return; return;
} }
Swal.close(); toast({ title: "Berhasil menghapus user" });
toast({ // ⬅️ INI YANG PENTING → REFRESH TABLE TANPA RELOAD
title: "Berhasil menghapus user", onRefresh();
});
router.push("?dataChange=true");
}; };
const handleDelete = (id: number) => { const handleDelete = (id: number) => {
MySwal.fire({ MySwal.fire({
title: "Apakah anda ingin menghapus data user?", title: "Hapus user ini?",
showCancelButton: true, showCancelButton: true,
confirmButtonColor: "#dc3545", confirmButtonColor: "#dc3545",
confirmButtonText: "Iya", confirmButtonText: "Iya",
cancelButtonText: "Tidak", cancelButtonText: "Tidak",
}).then((result) => { }).then((res) => {
if (result.isConfirmed) { if (res.isConfirmed) doDelete(id);
doDelete(id);
}
}); });
}; };
// const doDelete = async (id: number) => {
// const response = await deleteUser(id);
// if (response?.error) {
// toast({
// title: stringify(response?.message),
// variant: "destructive",
// });
// }
// toast({
// title: "Success delete",
// });
// router.push("?dataChange=true");
// };
// const handleDelete = (id: number) => {
// MySwal.fire({
// title: "Apakah anda ingin menghapus data user?",
// showCancelButton: true,
// confirmButtonColor: "#dc3545",
// confirmButtonText: "Iya",
// cancelButtonText: "Tidak",
// }).then((result) => {
// if (result.isConfirmed) {
// doDelete(id);
// }
// });
// };
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button size="icon" variant="ghost">
size="icon" <MoreVertical className="h-4 w-4" />
className="bg-transparent ring-offset-transparent hover:bg-transparent hover:ring-0 hover:ring-transparent"
>
<MoreVertical className="h-4 w-4 text-default-800" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="p-0" align="end"> <DropdownMenuContent align="end">
<Link href={`/admin/add-experts/detail/${row?.original?.id}`}>
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none"> <Link href={`/admin/add-experts/detail/${row.original.id}`}>
<Eye className="w-4 h-4 me-1.5" /> <DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none cursor-pointer">
View <Eye className="w-4 h-4 me-1.5" /> View
</DropdownMenuItem> </DropdownMenuItem>
</Link> </Link>
<Link href={`/admin/add-experts/update/${row?.original?.id}`}> <Link href={`/admin/add-experts/update/${row.original.id}`}>
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none"> <DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none cursor-pointer">
<SquarePen className="w-4 h-4 me-1.5" /> <SquarePen className="w-4 h-4 me-1.5" /> Edit
Edit
</DropdownMenuItem> </DropdownMenuItem>
</Link> </Link>
<DropdownMenuItem <DropdownMenuItem
onClick={() => handleDelete(row.original.userKeycloakId)} onClick={() => handleDelete(row.original.userKeycloakId)}
className="p-2 border-b text-destructive bg-destructive/30 focus:bg-destructive focus:text-destructive-foreground rounded-none" className="text-red-600 cursor-pointer hover:bg-red-300"
> >
<Trash2 className="w-4 h-4 me-1.5" /> <Trash2 className="w-4 h-4 me-1.5" /> Delete
Delete
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
); );
@ -200,4 +162,4 @@ const columns: ColumnDef<any>[] = [
}, },
]; ];
export default columns; export default getColumns;

View File

@ -2,7 +2,6 @@
import * as React from "react"; import * as React from "react";
import { import {
ColumnDef,
ColumnFiltersState, ColumnFiltersState,
PaginationState, PaginationState,
SortingState, SortingState,
@ -15,7 +14,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,
@ -25,7 +23,6 @@ import {
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import { UserIcon } from "lucide-react"; import { UserIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@ -35,43 +32,14 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { InputGroup, InputGroupText } from "@/components/ui/input-group";
import { paginationBlog } from "@/service/blog/blog";
import { ticketingPagination } from "@/service/ticketing/ticketing";
import { Badge } from "@/components/ui/badge";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import TablePagination from "@/components/table/table-pagination"; import TablePagination from "@/components/table/table-pagination";
import columns from "./column"; // import columns from "./column";
import { getPlanningPagination } from "@/service/agenda-setting/agenda-setting"; import getColumns from "./column";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { listDataMedia } from "@/service/broadcast/broadcast";
import { listEnableCategory } from "@/service/content/content"; import { listEnableCategory } from "@/service/content/content";
import { Checkbox } from "@/components/ui/checkbox";
import { close, loading } from "@/config/swal";
import { Link } from "@/i18n/routing"; import { Link } from "@/i18n/routing";
import { listDataExperts } from "@/service/experts/experts"; import { listDataExperts } from "@/service/experts/experts";
const dummyData = [
{
id: 1,
name: "Prof. Dr. Ravi",
region: "Nasional",
skills: "Komunikasi",
experience: "Akademisi",
},
{
id: 2,
name: "Prof. Dr. Novan",
region: "DKI Jakarta",
skills: "Hukum",
experience: "Akademisi + Praktisi",
},
];
const AddExpertTable = () => { const AddExpertTable = () => {
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
@ -97,7 +65,8 @@ const AddExpertTable = () => {
const [limit, setLimit] = React.useState(10); const [limit, setLimit] = React.useState(10);
const table = useReactTable({ const table = useReactTable({
data: dataTable, data: dataTable,
columns, // columns,
columns: getColumns({ onRefresh: fetchData }),
onSortingChange: setSorting, onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters, onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
@ -283,7 +252,11 @@ const AddExpertTable = () => {
)) ))
) : ( ) : (
<TableRow> <TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center"> <TableCell
// colSpan={columns.length}
colSpan={table.getAllLeafColumns().length}
className="h-24 text-center"
>
No results. No results.
</TableCell> </TableCell>
</TableRow> </TableRow>

View File

@ -62,29 +62,34 @@ import { Eye, EyeOff } from "lucide-react";
// }), // }),
// }); // });
const FormSchema = z const FormSchema = z.object({
.object({ name: z.string({ required_error: "Required" }),
name: z.string({ required_error: "Required" }), username: z
username: z.string({ required_error: "Required" }), .string({ required_error: "Required" })
password: z .refine((val) => !/\s/.test(val), {
.string({ required_error: "Required" }) message: "Username tidak boleh mengandung spasi",
.min(8, "Minimal 8 karakter") }),
.regex(/[A-Z]/, "Harus mengandung huruf besar (A-Z)") // .transform((val) => val.toLowerCase()),
.regex(/[0-9]/, "Harus mengandung angka (0-9)")
.regex(/[^A-Za-z0-9]/, "Harus mengandung karakter spesial (!@#$%^&*)"),
// confirmPassword: z.string({ required_error: "Required" }), password: z
.string({ required_error: "Required" })
.min(8, "Minimal 8 karakter")
.regex(/[A-Z]/, "Harus mengandung huruf besar (A-Z)")
.regex(/[0-9]/, "Harus mengandung angka (0-9)")
.regex(/[^A-Za-z0-9]/, "Harus mengandung karakter spesial (!@#$%^&*)"),
phoneNumber: z.string({ required_error: "Required" }), // confirmPassword: z.string({ required_error: "Required" }),
email: z.string({ required_error: "Required" }),
skills: z.string({ required_error: "Required" }), phoneNumber: z.string({ required_error: "Required" }),
experiences: z.string({ required_error: "Required" }), email: z.string({ required_error: "Required" }),
company: z.string({ required_error: "Required" }), skills: z.string({ required_error: "Required" }),
}) experiences: z.string({ required_error: "Required" }),
// .refine((data) => data.password === data.confirmPassword, { company: z.string({ required_error: "Required" }),
// path: ["confirmPassword"], });
// message: "Konfirmasi password tidak sama", // .refine((data) => data.password === data.confirmPassword, {
// }); // path: ["confirmPassword"],
// message: "Konfirmasi password tidak sama",
// });
export type Placements = { export type Placements = {
index: number; index: number;
@ -326,6 +331,39 @@ export default function AddExpertForm() {
)} )}
/> />
<FormField <FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username (huruf kecil, tanpa spasi)</FormLabel>
<Input
type="text"
value={field.value}
placeholder="masukkan username"
onChange={(e) => {
let value = e.target.value;
// Hapus spasi otomatis
value = value.replace(/\s+/g, "");
// Jadikan lowercase otomatis
value = value.toLowerCase();
field.onChange(value);
}}
/>
{/* Info tambahan */}
<p className="text-xs text-gray-500 mt-1">
Username otomatis menjadi huruf kecil tanpa spasi.
</p>
<FormMessage />
</FormItem>
)}
/>
{/* <FormField
control={form.control} control={form.control}
name="username" name="username"
render={({ field }) => ( render={({ field }) => (
@ -341,7 +379,7 @@ export default function AddExpertForm() {
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> /> */}
<FormField <FormField
control={form.control} control={form.control}
name="phoneNumber" name="phoneNumber"
@ -411,7 +449,10 @@ export default function AddExpertForm() {
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />} {showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
</button> </button>
</div> </div>
<FormLabel className="text-gray-400 text-[12px]">Password harus memiliki minimal 8 karakter, special karakter, angka dan huruf kapital</FormLabel> <FormLabel className="text-gray-400 text-[12px]">
Password harus memiliki minimal 8 karakter, special karakter,
angka dan huruf kapital
</FormLabel>
{/* Strength meter */} {/* Strength meter */}
{field.value && ( {field.value && (

View File

@ -59,8 +59,12 @@ import {
deleteMediaBlastCampaignAccount, deleteMediaBlastCampaignAccount,
saveMediaBlastCampaignAccountBulk, saveMediaBlastCampaignAccountBulk,
} from "@/service/broadcast/broadcast"; } from "@/service/broadcast/broadcast";
import { AdministrationUserList, getUserListAll } from "@/service/management-user/management-user"; import {
AdministrationUserList,
getUserListAll,
} from "@/service/management-user/management-user";
import { close, loading, error, success, successCallback } from "@/config/swal"; import { close, loading, error, success, successCallback } from "@/config/swal";
import { Link } from "@/i18n/routing";
// Mock data for available accounts - replace with actual API call // Mock data for available accounts - replace with actual API call
const availableAccounts = [ const availableAccounts = [
@ -98,7 +102,8 @@ const AccountListTable = () => {
const [accountCategory, setAccountCategory] = React.useState<string>(""); const [accountCategory, setAccountCategory] = React.useState<string>("");
const [selectedAccount, setSelectedAccount] = React.useState<any[]>([]); const [selectedAccount, setSelectedAccount] = React.useState<any[]>([]);
const [selectedCategory, setSelectedCategory] = React.useState<string>(""); const [selectedCategory, setSelectedCategory] = React.useState<string>("");
const [availableAccountsList, setAvailableAccountsList] = React.useState<any[]>(availableAccounts); const [availableAccountsList, setAvailableAccountsList] =
React.useState<any[]>(availableAccounts);
const [usersList, setUsersList] = React.useState<any[]>([]); const [usersList, setUsersList] = React.useState<any[]>([]);
const table = useReactTable({ const table = useReactTable({
@ -171,7 +176,7 @@ const AccountListTable = () => {
async function saveCampaignAccount() { async function saveCampaignAccount() {
try { try {
loading(); loading();
if (accountCategory === "all-account") { if (accountCategory === "all-account") {
// Handle all accounts - send only campaignId and category "all" // Handle all accounts - send only campaignId and category "all"
const request = { const request = {
@ -202,7 +207,7 @@ const AccountListTable = () => {
default: default:
roleId = "5"; roleId = "5";
} }
const request = { const request = {
mediaBlastCampaignId: campaignId, mediaBlastCampaignId: campaignId,
mediaBlastAccountCategory: `role-${roleId}`, mediaBlastAccountCategory: `role-${roleId}`,
@ -216,7 +221,7 @@ const AccountListTable = () => {
// Handle custom selection - send campaignId and selected user IDs // Handle custom selection - send campaignId and selected user IDs
const request = { const request = {
mediaBlastCampaignId: campaignId, mediaBlastCampaignId: campaignId,
mediaBlastAccountIds: selectedAccount.map(acc => acc.id), mediaBlastAccountIds: selectedAccount.map((acc) => acc.id),
}; };
const response = await saveMediaBlastCampaignAccountBulk(request); const response = await saveMediaBlastCampaignAccountBulk(request);
if (response?.error) { if (response?.error) {
@ -224,7 +229,7 @@ const AccountListTable = () => {
return; return;
} }
} }
close(); close();
successCallback("Akun berhasil ditambahkan ke campaign!"); successCallback("Akun berhasil ditambahkan ke campaign!");
resetDialogState(); resetDialogState();
@ -247,7 +252,7 @@ const AccountListTable = () => {
try { try {
loading(); loading();
const response = await getUserListAll(); const response = await getUserListAll();
if (response?.data?.data?.content) { if (response?.data?.data?.content) {
setUsersList(response.data.data.content); setUsersList(response.data.data.content);
} }
@ -265,15 +270,15 @@ const AccountListTable = () => {
setFiltered(temp); setFiltered(temp);
}; };
const removeSelectedAccount = (accountId: string) => { const removeSelectedAccount = (accountId: string) => {
setSelectedAccount(selectedAccount.filter(acc => acc.id !== accountId)); setSelectedAccount(selectedAccount.filter((acc) => acc.id !== accountId));
}; };
const getFilteredAccounts = () => { const getFilteredAccounts = () => {
if (accountCategory === "kategori" && selectedCategory) { if (accountCategory === "kategori" && selectedCategory) {
return availableAccountsList.filter(acc => acc.category === selectedCategory); return availableAccountsList.filter(
(acc) => acc.category === selectedCategory
);
} }
return availableAccountsList; return availableAccountsList;
}; };
@ -291,7 +296,10 @@ const AccountListTable = () => {
Pilih Akun Pilih Akun
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent size="md" className="max-w-xl max-h-[80vh] overflow-y-auto"> <DialogContent
size="md"
className="max-w-xl max-h-[80vh] overflow-y-auto"
>
<DialogHeader> <DialogHeader>
<DialogTitle>Pilih Akun Untuk Campaign Ini</DialogTitle> <DialogTitle>Pilih Akun Untuk Campaign Ini</DialogTitle>
</DialogHeader> </DialogHeader>
@ -350,15 +358,17 @@ const AccountListTable = () => {
options={usersList.map((user: any) => ({ options={usersList.map((user: any) => ({
value: user.id, value: user.id,
label: `${user.fullname} (${user.role?.name})`, label: `${user.fullname} (${user.role?.name})`,
user: user user: user,
}))} }))}
value={selectedAccount.map((acc: any) => ({ value={selectedAccount.map((acc: any) => ({
value: acc.id, value: acc.id,
label: `${acc.fullname} (${acc.role?.name})`, label: `${acc.fullname} (${acc.role?.name})`,
user: acc user: acc,
}))} }))}
onChange={(selectedOptions: any) => { onChange={(selectedOptions: any) => {
const selectedUsers = selectedOptions ? selectedOptions.map((option: any) => option.user) : []; const selectedUsers = selectedOptions
? selectedOptions.map((option: any) => option.user)
: [];
setSelectedAccount(selectedUsers); setSelectedAccount(selectedUsers);
}} }}
placeholder="Cari dan pilih user..." placeholder="Cari dan pilih user..."
@ -369,14 +379,17 @@ const AccountListTable = () => {
className="react-select" className="react-select"
classNamePrefix="select" classNamePrefix="select"
/> />
{/* Selected Accounts Display */} {/* Selected Accounts Display */}
{selectedAccount.length > 0 && ( {selectedAccount.length > 0 && (
<div className="space-y-2"> <div className="space-y-2">
<Label>User Terpilih ({selectedAccount.length}):</Label> <Label>User Terpilih ({selectedAccount.length}):</Label>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{selectedAccount.map((acc) => ( {selectedAccount.map((acc) => (
<Badge key={acc.id} className="flex items-center gap-1"> <Badge
key={acc.id}
className="flex items-center gap-1"
>
{acc.fullname} {acc.fullname}
<X <X
className="h-3 w-3 cursor-pointer" className="h-3 w-3 cursor-pointer"
@ -403,7 +416,8 @@ const AccountListTable = () => {
{accountCategory === "kategori" && selectedCategory && ( {accountCategory === "kategori" && selectedCategory && (
<div className="p-3 bg-green-50 rounded-md"> <div className="p-3 bg-green-50 rounded-md">
<p className="text-sm text-green-700"> <p className="text-sm text-green-700">
Semua akun dengan role "{selectedCategory.toUpperCase()}" akan ditambahkan. Semua akun dengan role "{selectedCategory.toUpperCase()}"
akan ditambahkan.
</p> </p>
</div> </div>
)} )}
@ -412,7 +426,8 @@ const AccountListTable = () => {
{accountCategory === "custom" && ( {accountCategory === "custom" && (
<div className="p-3 bg-purple-50 rounded-md"> <div className="p-3 bg-purple-50 rounded-md">
<p className="text-sm text-purple-700"> <p className="text-sm text-purple-700">
{selectedAccount.length} user terpilih akan ditambahkan ke campaign ini. {selectedAccount.length} user terpilih akan ditambahkan ke
campaign ini.
</p> </p>
</div> </div>
)} )}
@ -423,7 +438,8 @@ const AccountListTable = () => {
onClick={saveCampaignAccount} onClick={saveCampaignAccount}
disabled={ disabled={
!accountCategory || !accountCategory ||
(accountCategory === "custom" && selectedAccount.length < 1) || (accountCategory === "custom" &&
selectedAccount.length < 1) ||
(accountCategory === "kategori" && !selectedCategory) (accountCategory === "kategori" && !selectedCategory)
} }
> >
@ -441,7 +457,47 @@ const AccountListTable = () => {
</div> </div>
{/* === Filter Akun === */} {/* === Filter Akun === */}
<div className="flex justify-end"> <div className="flex flex-row justify-end">
{/* <div className="flex flex-row gap-4">
<Link href="/admin/broadcast/campaign-list/account-list/create">
<Button variant="default" className="bg-[#3f37c9] gap-2">
<span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="22"
height="22"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M17 13h-4v4h-2v-4H7v-2h4V7h2v4h4m2-8H5c-1.11 0-2 .89-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2"
/>
</svg>
</span>
Tambahkan Akun
</Button>
</Link>
<Button variant="default" className="bg-[#3f37c9] gap-2">
<span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<g fill="none">
<path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z" />
<path
fill="currentColor"
d="M12 2v6.5a1.5 1.5 0 0 0 1.5 1.5H20v10a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-1h3.414l-1.121 1.121a1 1 0 1 0 1.414 1.415l2.829-2.829a1 1 0 0 0 0-1.414l-2.829-2.828a1 1 0 1 0-1.414 1.414L7.414 17H4V4a2 2 0 0 1 2-2zM4 17v2H3a1 1 0 1 1 0-2zM14 2.043a2 2 0 0 1 1 .543L19.414 7a2 2 0 0 1 .543 1H14z"
/>
</g>
</svg>
</span>
Import Akun
</Button>
</div> */}
<Popover> <Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button size="md" variant="outline"> <Button size="md" variant="outline">

View File

@ -6,7 +6,6 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { import {
Form, Form,
FormControl, FormControl,
FormDescription,
FormField, FormField,
FormItem, FormItem,
FormLabel, FormLabel,
@ -14,55 +13,80 @@ import {
} from "@/components/ui/form"; } from "@/components/ui/form";
import withReactContent from "sweetalert2-react-content"; import withReactContent from "sweetalert2-react-content";
import Swal from "sweetalert2"; import Swal from "sweetalert2";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
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 { cn } from "@/lib/utils";
import { format } from "date-fns";
import { CalendarIcon } from "lucide-react";
import { Calendar } from "@/components/ui/calendar";
import { getOnlyDate } from "@/utils/globals";
import { import {
getMediaBlastCampaignPage,
saveMediaBlastAccount, saveMediaBlastAccount,
saveMediaBlastCampaign,
} from "@/service/broadcast/broadcast"; } from "@/service/broadcast/broadcast";
import { error } from "@/config/swal"; import { error } from "@/config/swal";
import { useRouter } from "@/i18n/routing"; import { useRouter } from "@/i18n/routing";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { useEffect, useState } from "react";
// ----------------------------
// ZOD SCHEMA (dinamis)
// ----------------------------
const FormSchema = z.object({ const FormSchema = z.object({
name: z.string({ name: z.string({ required_error: "Required" }),
required_error: "Required",
}),
accountType: z accountType: z
.array(z.string()) .array(z.string())
.refine((value) => value.some((item) => item), { .min(1, "Pilih minimal satu tipe akun"),
message: "Required",
}), email: z.string().optional(),
accountCategory: z.enum(["polri", "jurnalis", "umum", "ksp"], { whatsapp: z.string().optional(),
required_error: "Required",
}), campaignId: z.string({ required_error: "Required" }),
email: z.string({ }).refine(
required_error: "Required", (data) => {
}), if (data.accountType.includes("email") && !data.email) return false;
whatsapp: z.string({ return true;
required_error: "Required", },
}), { message: "Email wajib diisi", path: ["email"] }
}); ).refine(
(data) => {
if (data.accountType.includes("wa") && !data.whatsapp) return false;
return true;
},
{ message: "Whatsapp wajib diisi", path: ["whatsapp"] }
);
// ----------------------------
// COMPONENT
// ----------------------------
export default function CreateAccountForBroadcast() { export default function CreateAccountForBroadcast() {
const MySwal = withReactContent(Swal); const MySwal = withReactContent(Swal);
const router = useRouter(); const router = useRouter();
const form = useForm<z.infer<typeof FormSchema>>({ const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema), resolver: zodResolver(FormSchema),
defaultValues: { accountType: [] }, defaultValues: {
accountType: [],
email: "",
whatsapp: "",
},
}); });
const selectedTypes = form.watch("accountType");
const [campaigns, setCampaigns] = useState<any[]>([]);
useEffect(() => {
fetchCampaignList();
}, []);
async function fetchCampaignList() {
try {
const res = await getMediaBlastCampaignPage(0);
setCampaigns(res?.data?.data?.content ?? []);
} catch (e) {
console.log("Error fetch campaign:", e);
}
}
const onSubmit = async (data: z.infer<typeof FormSchema>) => { const onSubmit = async (data: z.infer<typeof FormSchema>) => {
MySwal.fire({ MySwal.fire({
title: "Simpan Data", title: "Simpan Data",
@ -85,10 +109,8 @@ export default function CreateAccountForBroadcast() {
icon: "success", icon: "success",
confirmButtonColor: "#3085d6", confirmButtonColor: "#3085d6",
confirmButtonText: "OK", confirmButtonText: "OK",
}).then((result) => { }).then(() => {
if (result.isConfirmed) { router.push("/admin/broadcast/campaign-list/account-list");
router.push("/admin/broadcast/campaign-list/account-list");
}
}); });
} }
@ -96,20 +118,21 @@ export default function CreateAccountForBroadcast() {
const reqData = { const reqData = {
accountName: data.name, accountName: data.name,
accountType: data.accountType.join(","), accountType: data.accountType.join(","),
accountCategory: data.accountCategory, emailAddress: data.email ?? "",
emailAddress: data.email, whatsappNumber: data.whatsapp ?? "",
whatsappNumber: data.whatsapp, campaignId: data.campaignId,
}; };
console.log("data", data);
console.log("REQ:", reqData);
const response = await saveMediaBlastAccount(reqData); const response = await saveMediaBlastAccount(reqData);
if (response?.error) { if (response?.error) {
error(response.message); error(response.message);
return false; return;
} }
successSubmit(); successSubmit();
}; };
return ( return (
<div> <div>
<SiteBreadcrumb /> <SiteBreadcrumb />
@ -118,7 +141,9 @@ export default function CreateAccountForBroadcast() {
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}
className="space-y-3 bg-white rounded-sm p-4" className="space-y-3 bg-white rounded-sm p-4"
> >
<p className="fonnt-semibold">Account</p> <p className="font-semibold">Account</p>
{/* NAMA */}
<FormField <FormField
control={form.control} control={form.control}
name="name" name="name"
@ -130,172 +155,125 @@ export default function CreateAccountForBroadcast() {
placeholder="Masukkan nama" placeholder="Masukkan nama"
onChange={field.onChange} onChange={field.onChange}
/> />
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
{/* CHECKBOX TIPE AKUN */}
<FormField <FormField
control={form.control} control={form.control}
name="accountType" name="accountType"
render={() => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Tipe Akun</FormLabel> <FormLabel>Tipe Akun</FormLabel>
<div className="flex flex-row gap-2"> <div className="flex flex-row gap-4">
{" "} {/* WA */}
<FormField <div className="flex items-center gap-2">
key="wa" <Checkbox
control={form.control} checked={field.value.includes("wa")}
name="accountType" onCheckedChange={(checked) =>
render={({ field }) => { checked
return ( ? field.onChange([...field.value, "wa"])
<FormItem : field.onChange(field.value.filter((v) => v !== "wa"))
key="wa" }
className="flex flex-row items-start space-x-3 space-y-0" />
> <label>Whatsapp</label>
<FormControl> </div>
<Checkbox
checked={field.value?.includes("wa")}
onCheckedChange={(checked) => {
return checked
? field.onChange([...field.value, "wa"])
: field.onChange(
field.value?.filter(
(value) => value !== "wa"
)
);
}}
/>
</FormControl>
<FormLabel className="font-normal">
Whatsapp
</FormLabel>
</FormItem>
);
}}
/>
<FormField
key="email"
control={form.control}
name="accountType"
render={({ field }) => {
return (
<FormItem
key="email"
className="flex flex-row items-start space-x-3 space-y-0"
>
<FormControl>
<Checkbox
checked={field.value?.includes("email")}
onCheckedChange={(checked) => {
return checked
? field.onChange([...field.value, "email"])
: field.onChange(
field.value?.filter(
(value) => value !== "email"
)
);
}}
/>
</FormControl>
<FormLabel className="font-normal">Email</FormLabel>
</FormItem>
);
}}
/>
</div>
{/* EMAIL */}
<div className="flex items-center gap-2">
<Checkbox
checked={field.value.includes("email")}
onCheckedChange={(checked) =>
checked
? field.onChange([...field.value, "email"])
: field.onChange(
field.value.filter((v) => v !== "email")
)
}
/>
<label>Email</label>
</div>
</div>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
{/* FORM WHATSAPP */}
{selectedTypes.includes("wa") && (
<FormField
control={form.control}
name="whatsapp"
render={({ field }) => (
<FormItem>
<FormLabel>Whatsapp</FormLabel>
<Input
type="number"
placeholder="Masukkan nomor Whatsapp"
{...field}
/>
<FormMessage />
</FormItem>
)}
/>
)}
{/* FORM EMAIL */}
{selectedTypes.includes("email") && (
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<Input
type="email"
placeholder="Masukkan email"
{...field}
/>
<FormMessage />
</FormItem>
)}
/>
)}
{/* CAMPAIGN */}
<FormField <FormField
control={form.control} control={form.control}
name="accountCategory" name="campaignId"
render={({ field }) => ( render={({ field }) => (
<FormItem className="space-y-3"> <FormItem>
<FormLabel>Kategori</FormLabel> <FormLabel>Campaign</FormLabel>
<FormControl> <FormControl>
<RadioGroup <select
onValueChange={field.onChange} className="w-full border rounded-md p-2 text-sm"
defaultValue={field.value} value={field.value}
className="flex flex-row gap-2" onChange={field.onChange}
> >
<FormItem className="flex items-center space-x-3 space-y-0"> <option value="" className="text-slate-400">
<FormControl> Pilih campaign
<RadioGroupItem value="polri" /> </option>
</FormControl>
<FormLabel className="font-normal">POLRI</FormLabel> {campaigns.map((c: any) => (
</FormItem> <option key={c.id} value={c.id}>
<FormItem className="flex items-center space-x-3 space-y-0"> {c.title || `Campaign ${c.id}`}
<FormControl> </option>
<RadioGroupItem value="jurnalis" /> ))}
</FormControl> </select>
<FormLabel className="font-normal">JURNALIS</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="umum" />
</FormControl>
<FormLabel className="font-normal">UMUM</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="ksp" />
</FormControl>
<FormLabel className="font-normal">KSP</FormLabel>
</FormItem>
</RadioGroup>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Nama</FormLabel>
<Input
type="email"
value={field.value}
placeholder="Masukkan email"
onChange={field.onChange}
/>
<FormMessage /> {/* BUTTON */}
</FormItem>
)}
/>
<FormField
control={form.control}
name="whatsapp"
render={({ field }) => (
<FormItem>
<FormLabel>Nama</FormLabel>
<Input
type="number"
value={field.value}
placeholder="Masukkan whatsapp"
onChange={field.onChange}
/>
<FormMessage />
</FormItem>
)}
/>
<div className="flex flex-row gap-2 mt-4 pt-4"> <div className="flex flex-row gap-2 mt-4 pt-4">
<Button <Button type="button" variant="outline" color="destructive">
size="md"
type="button"
variant="outline"
color="destructive"
className="text-xs"
>
Cancel Cancel
</Button> </Button>
<Button size="md" type="submit" color="primary" className="text-xs"> <Button type="submit" color="primary">
Submit Submit
</Button> </Button>
</div> </div>
@ -304,3 +282,380 @@ export default function CreateAccountForBroadcast() {
</div> </div>
); );
} }
// "use client";
// import SiteBreadcrumb from "@/components/site-breadcrumb";
// import { z } from "zod";
// import { useForm } from "react-hook-form";
// import { zodResolver } from "@hookform/resolvers/zod";
// import {
// Form,
// FormControl,
// FormField,
// FormItem,
// FormLabel,
// FormMessage,
// } from "@/components/ui/form";
// import withReactContent from "sweetalert2-react-content";
// import Swal from "sweetalert2";
// import { Input } from "@/components/ui/input";
// import { Button } from "@/components/ui/button";
// import {
// getMediaBlastCampaignPage,
// saveMediaBlastAccount,
// } from "@/service/broadcast/broadcast";
// import { error } from "@/config/swal";
// import { useRouter } from "@/i18n/routing";
// import { Checkbox } from "@/components/ui/checkbox";
// import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
// import { useEffect, useState } from "react";
// // const FormSchema = z.object({
// // name: z.string({
// // required_error: "Required",
// // }),
// // accountType: z
// // .array(z.string())
// // .refine((value) => value.some((item) => item), {
// // message: "Required",
// // }),
// // accountCategory: z.enum(["polri", "jurnalis", "umum", "ksp"], {
// // required_error: "Required",
// // }),
// // email: z.string({
// // required_error: "Required",
// // }),
// // whatsapp: z.string({
// // required_error: "Required",
// // }),
// // campaignId: z.string({ required_error: "Required" }),
// // });
// const FormSchema = z
// .object({
// name: z.string().min(1, "Required"),
// accountType: z.array(z.string()).refine((value) => value.length > 0, {
// message: "Pilih minimal satu tipe akun",
// }),
// accountCategory: z.enum(["polri", "jurnalis", "umum", "ksp"], {
// required_error: "Required",
// }),
// email: z.string().optional(),
// whatsapp: z.string().optional(),
// campaignId: z.string().min(1, "Required"),
// })
// .refine(
// (data) => {
// if (data.accountType.includes("email")) {
// return !!data.email && data.email.trim() !== "";
// }
// return true;
// },
// { path: ["email"], message: "Email wajib diisi" }
// )
// .refine(
// (data) => {
// if (data.accountType.includes("wa")) {
// return !!data.whatsapp && data.whatsapp.trim() !== "";
// }
// return true;
// },
// { path: ["whatsapp"], message: "Whatsapp wajib diisi" }
// );
// export default function CreateAccountForBroadcast() {
// const MySwal = withReactContent(Swal);
// const router = useRouter();
// const form = useForm<z.infer<typeof FormSchema>>({
// resolver: zodResolver(FormSchema),
// defaultValues: { accountType: [] },
// });
// const selectedTypes = form.watch("accountType");
// const [campaigns, setCampaigns] = useState<any[]>([]);
// useEffect(() => {
// fetchCampaignList();
// }, []);
// async function fetchCampaignList() {
// try {
// const res = await getMediaBlastCampaignPage(0);
// setCampaigns(res?.data?.data?.content ?? []);
// } catch (e) {
// console.log("Error fetch campaign:", e);
// }
// }
// const onSubmit = async (data: z.infer<typeof FormSchema>) => {
// MySwal.fire({
// title: "Simpan Data",
// text: "Apakah Anda yakin ingin menyimpan data ini?",
// icon: "warning",
// showCancelButton: true,
// cancelButtonColor: "#d33",
// confirmButtonColor: "#3085d6",
// confirmButtonText: "Simpan",
// }).then((result) => {
// if (result.isConfirmed) {
// save(data);
// }
// });
// };
// function successSubmit() {
// MySwal.fire({
// title: "Sukses",
// icon: "success",
// confirmButtonColor: "#3085d6",
// confirmButtonText: "OK",
// }).then((result) => {
// if (result.isConfirmed) {
// router.push("/admin/broadcast/campaign-list/account-list");
// }
// });
// }
// const save = async (data: z.infer<typeof FormSchema>) => {
// const reqData = {
// accountName: data.name,
// accountType: data.accountType.join(","),
// accountCategory: data.accountCategory,
// emailAddress: data.email ?? "",
// whatsappNumber: data.whatsapp ?? "",
// campaignId: data.campaignId,
// };
// console.log("data", data);
// const response = await saveMediaBlastAccount(reqData);
// if (response?.error) {
// error(response.message);
// return false;
// }
// successSubmit();
// };
// return (
// <div>
// <SiteBreadcrumb />
// <Form {...form}>
// <form
// onSubmit={form.handleSubmit(onSubmit)}
// className="space-y-3 bg-white rounded-sm p-4"
// >
// <p className="fonnt-semibold">Account</p>
// <FormField
// control={form.control}
// name="name"
// render={({ field }) => (
// <FormItem>
// <FormLabel>Nama</FormLabel>
// <Input
// value={field.value}
// placeholder="Masukkan nama"
// onChange={field.onChange}
// />
// <FormMessage />
// </FormItem>
// )}
// />
// <FormField
// control={form.control}
// name="accountType"
// render={() => (
// <FormItem>
// <FormLabel>Tipe Akun</FormLabel>
// <div className="flex flex-row gap-2">
// {" "}
// <FormField
// key="wa"
// control={form.control}
// name="accountType"
// render={({ field }) => {
// return (
// <FormItem
// key="wa"
// className="flex flex-row items-start space-x-3 space-y-0"
// >
// <FormControl>
// <Checkbox
// checked={field.value?.includes("wa")}
// onCheckedChange={(checked) => {
// return checked
// ? field.onChange([...field.value, "wa"])
// : field.onChange(
// field.value?.filter(
// (value) => value !== "wa"
// )
// );
// }}
// />
// </FormControl>
// <FormLabel className="font-normal">
// Whatsapp
// </FormLabel>
// </FormItem>
// );
// }}
// />
// <FormField
// key="email"
// control={form.control}
// name="accountType"
// render={({ field }) => {
// return (
// <FormItem
// key="email"
// className="flex flex-row items-start space-x-3 space-y-0"
// >
// <FormControl>
// <Checkbox
// checked={field.value?.includes("email")}
// onCheckedChange={(checked) => {
// return checked
// ? field.onChange([...field.value, "email"])
// : field.onChange(
// field.value?.filter(
// (value) => value !== "email"
// )
// );
// }}
// />
// </FormControl>
// <FormLabel className="font-normal">Email</FormLabel>
// </FormItem>
// );
// }}
// />
// </div>
// <FormMessage />
// </FormItem>
// )}
// />
// {/* <FormField
// control={form.control}
// name="accountCategory"
// render={({ field }) => (
// <FormItem className="space-y-3">
// <FormLabel>Kategori</FormLabel>
// <FormControl>
// <RadioGroup
// onValueChange={field.onChange}
// defaultValue={field.value}
// className="flex flex-row gap-2"
// >
// <FormItem className="flex items-center space-x-3 space-y-0">
// <FormControl>
// <RadioGroupItem value="polri" />
// </FormControl>
// <FormLabel className="font-normal">POLRI</FormLabel>
// </FormItem>
// <FormItem className="flex items-center space-x-3 space-y-0">
// <FormControl>
// <RadioGroupItem value="jurnalis" />
// </FormControl>
// <FormLabel className="font-normal">JURNALIS</FormLabel>
// </FormItem>
// <FormItem className="flex items-center space-x-3 space-y-0">
// <FormControl>
// <RadioGroupItem value="umum" />
// </FormControl>
// <FormLabel className="font-normal">UMUM</FormLabel>
// </FormItem>
// <FormItem className="flex items-center space-x-3 space-y-0">
// <FormControl>
// <RadioGroupItem value="ksp" />
// </FormControl>
// <FormLabel className="font-normal">KSP</FormLabel>
// </FormItem>
// </RadioGroup>
// </FormControl>
// <FormMessage />
// </FormItem>
// )}
// /> */}
// <FormField
// control={form.control}
// name="email"
// render={({ field }) => (
// <FormItem>
// <FormLabel>Email</FormLabel>
// <Input
// type="email"
// value={field.value}
// placeholder="Masukkan email"
// onChange={field.onChange}
// />
// <FormMessage />
// </FormItem>
// )}
// />
// <FormField
// control={form.control}
// name="whatsapp"
// render={({ field }) => (
// <FormItem>
// <FormLabel>Nama</FormLabel>
// <Input
// type="number"
// value={field.value}
// placeholder="Masukkan whatsapp"
// onChange={field.onChange}
// />
// <FormMessage />
// </FormItem>
// )}
// />
// <FormField
// control={form.control}
// name="campaignId"
// render={({ field }) => (
// <FormItem>
// <FormLabel>Campaign</FormLabel>
// <FormControl>
// <select
// className="w-full border rounded-md p-2"
// value={field.value}
// onChange={field.onChange}
// >
// <option value="" className="text-slate-400">
// Pilih campaign
// </option>
// {campaigns.map((c: any) => (
// <option key={c.id} value={c.id}>
// {c.title || `Campaign ${c.id}`}
// </option>
// ))}
// </select>
// </FormControl>
// <FormMessage />
// </FormItem>
// )}
// />
// <div className="flex flex-row gap-2 mt-4 pt-4">
// <Button
// size="md"
// type="button"
// variant="outline"
// color="destructive"
// className="text-xs"
// >
// Cancel
// </Button>
// <Button size="md" type="submit" color="primary" className="text-xs">
// Submit
// </Button>
// </div>
// </form>
// </Form>
// </div>
// );
// }

View File

@ -90,16 +90,16 @@ const columns: ColumnDef<any>[] = [
Detail Detail
</DropdownMenuItem> </DropdownMenuItem>
</Link> </Link>
<Link href={`/admin/broadcast/email/${row.original.id}`}> <Link href={`/admin/broadcast/create/${row.original.id}`}>
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none cursor-pointer"> <DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none cursor-pointer">
Email Blast Email & Whatsapp Blast
</DropdownMenuItem> </DropdownMenuItem>
</Link> </Link>
<Link href={`/admin/broadcast/whatsapp/${row.original.id}`}> {/* <Link href={`/admin/broadcast/whatsapp/${row.original.id}`}>
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none cursor-pointer"> <DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none cursor-pointer">
Whatsapp Blast Whatsapp Blast
</DropdownMenuItem> </DropdownMenuItem>
</Link> </Link> */}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
); );

View File

@ -1,6 +1,6 @@
"use client"; "use client";
import SiteBreadcrumb from "@/components/site-breadcrumb"; import SiteBreadcrumb from "@/components/site-breadcrumb";
import BroadcastTable from "./email/component/table"; import BroadcastTable from "./create/component/table";
import { PlusIcon } from "lucide-react"; import { PlusIcon } from "lucide-react";
import EscalationTable from "../../shared/communication/escalation/components/escalation-table"; import EscalationTable from "../../shared/communication/escalation/components/escalation-table";
@ -8,7 +8,7 @@ import InternalTable from "../../shared/communication/internal/components/intern
import { useState } from "react"; import { useState } from "react";
import { Link } from "@/i18n/routing"; import { Link } from "@/i18n/routing";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import BroadcastEmailTable from "./email/component/table"; import BroadcastEmailTable from "./create/component/table";
import BroadcastWhatsAppTable from "./whatsapp/component/table"; import BroadcastWhatsAppTable from "./whatsapp/component/table";
export default function AdminBroadcast() { export default function AdminBroadcast() {

View File

@ -109,7 +109,9 @@ const EventModal = ({
const pathname = usePathname(); const pathname = usePathname();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [checkedLevels, setCheckedLevels] = useState<Set<number>>(new Set()); const [checkedLevels, setCheckedLevels] = useState<Set<number>>(new Set());
const [expandedPolda, setExpandedPolda] = useState<Record<number, boolean>>({}); const [expandedPolda, setExpandedPolda] = useState<Record<number, boolean>>(
{}
);
const [audioFile, setAudioFile] = useState<File | null>(null); const [audioFile, setAudioFile] = useState<File | null>(null);
const [isRecording, setIsRecording] = useState(false); const [isRecording, setIsRecording] = useState(false);
const [timer, setTimer] = useState<number>(120); const [timer, setTimer] = useState<number>(120);
@ -153,9 +155,11 @@ const EventModal = ({
}); });
// State untuk melacak apakah perubahan berasal dari checkbox Jenis Agenda // State untuk melacak apakah perubahan berasal dari checkbox Jenis Agenda
const [isUpdatingFromJenisAgenda, setIsUpdatingFromJenisAgenda] = useState(false); const [isUpdatingFromJenisAgenda, setIsUpdatingFromJenisAgenda] =
useState(false);
// State untuk melacak jenis perubahan spesifik // State untuk melacak jenis perubahan spesifik
const [jenisAgendaChangeType, setJenisAgendaChangeType] = useState<string>(""); const [jenisAgendaChangeType, setJenisAgendaChangeType] =
useState<string>("");
const levelNumber = Number(getCookiesDecrypt("ulne")) || 0; const levelNumber = Number(getCookiesDecrypt("ulne")) || 0;
const userLevelId = getCookiesDecrypt("ulie"); const userLevelId = getCookiesDecrypt("ulie");
@ -261,7 +265,11 @@ const EventModal = ({
// useEffect untuk sinkronisasi checkbox modal dengan Jenis Agenda // useEffect untuk sinkronisasi checkbox modal dengan Jenis Agenda
useEffect(() => { useEffect(() => {
if (listDest.length > 0 && isUpdatingFromJenisAgenda && jenisAgendaChangeType) { if (
listDest.length > 0 &&
isUpdatingFromJenisAgenda &&
jenisAgendaChangeType
) {
syncModalWithJenisAgenda(); syncModalWithJenisAgenda();
} }
}, [isUpdatingFromJenisAgenda, jenisAgendaChangeType]); }, [isUpdatingFromJenisAgenda, jenisAgendaChangeType]);
@ -273,44 +281,55 @@ const EventModal = ({
} }
}, [checkedLevels, isUpdatingFromJenisAgenda]); }, [checkedLevels, isUpdatingFromJenisAgenda]);
// Fungsi untuk update wilayahPublish berdasarkan checkbox modal // Fungsi untuk update wilayahPublish berdasarkan checkbox modal
const updateWilayahPublishFromModal = () => { const updateWilayahPublishFromModal = () => {
// Hanya update jika tidak sedang dalam proses update dari Jenis Agenda // Hanya update jika tidak sedang dalam proses update dari Jenis Agenda
if (!isUpdatingFromJenisAgenda && listDest.length > 0) { if (!isUpdatingFromJenisAgenda && listDest.length > 0) {
// Hitung item yang dipilih berdasarkan checkedLevels // Hitung item yang dipilih berdasarkan checkedLevels
const checkedPoldaCount = listDest.filter((item: any) => const checkedPoldaCount = listDest.filter(
item.levelNumber === 2 && (item: any) =>
item.name !== "SATKER POLRI" && item.levelNumber === 2 &&
checkedLevels.has(Number(item.id)) item.name !== "SATKER POLRI" &&
checkedLevels.has(Number(item.id))
).length; ).length;
const checkedPolresCount = listDest.reduce((total: number, item: any) => { const checkedPolresCount = listDest.reduce((total: number, item: any) => {
if (item.subDestination) { if (item.subDestination) {
return total + item.subDestination.filter((sub: any) => checkedLevels.has(Number(sub.id))).length; return (
total +
item.subDestination.filter((sub: any) =>
checkedLevels.has(Number(sub.id))
).length
);
} }
return total; return total;
}, 0); }, 0);
const satkerItem: any = listDest.find((item: any) => item.name === "SATKER POLRI"); const satkerItem: any = listDest.find(
const checkedSatkerCount = satkerItem ? ( (item: any) => item.name === "SATKER POLRI"
(checkedLevels.has(Number(satkerItem.id)) ? 1 : 0) + );
(satkerItem.subDestination?.filter((sub: any) => checkedLevels.has(Number(sub.id))).length || 0) const checkedSatkerCount = satkerItem
) : 0; ? (checkedLevels.has(Number(satkerItem.id)) ? 1 : 0) +
(satkerItem.subDestination?.filter((sub: any) =>
checkedLevels.has(Number(sub.id))
).length || 0)
: 0;
// Checkbox aktif jika ADA item yang dipilih dalam kategori tersebut // Checkbox aktif jika ADA item yang dipilih dalam kategori tersebut
const hasSelectedPolda = checkedPoldaCount > 0; const hasSelectedPolda = checkedPoldaCount > 0;
const hasSelectedPolres = checkedPolresCount > 0; const hasSelectedPolres = checkedPolresCount > 0;
const hasSelectedSatker = checkedSatkerCount > 0; const hasSelectedSatker = checkedSatkerCount > 0;
// Update arrays untuk backend // Update arrays untuk backend
const newSelectedPolda = listDest const newSelectedPolda = listDest
.filter((item: any) => .filter(
item.levelNumber === 2 && (item: any) =>
item.name !== "SATKER POLRI" && item.levelNumber === 2 &&
checkedLevels.has(Number(item.id)) item.name !== "SATKER POLRI" &&
checkedLevels.has(Number(item.id))
) )
.map((item: any) => String(item.id)); .map((item: any) => String(item.id));
const newSelectedPolres: string[] = []; const newSelectedPolres: string[] = [];
listDest.forEach((item: any) => { listDest.forEach((item: any) => {
if (item.subDestination) { if (item.subDestination) {
@ -321,7 +340,7 @@ const EventModal = ({
}); });
} }
}); });
const newSelectedSatker: string[] = []; const newSelectedSatker: string[] = [];
if (satkerItem) { if (satkerItem) {
if (checkedLevels.has(Number(satkerItem.id))) { if (checkedLevels.has(Number(satkerItem.id))) {
@ -335,51 +354,56 @@ const EventModal = ({
}); });
} }
} }
// Update state arrays // Update state arrays
setSelectedPolda(newSelectedPolda); setSelectedPolda(newSelectedPolda);
setSelectedPolres(newSelectedPolres); setSelectedPolres(newSelectedPolres);
setSelectedSatker(newSelectedSatker); setSelectedSatker(newSelectedSatker);
// Update wilayahPublish berdasarkan yang dipilih di modal // Update wilayahPublish berdasarkan yang dipilih di modal
setWilayahPublish(prev => { setWilayahPublish((prev) => {
const newState = { ...prev }; const newState = { ...prev };
// Update individual checkboxes // Update individual checkboxes
newState.polda = hasSelectedPolda; newState.polda = hasSelectedPolda;
newState.polres = hasSelectedPolres; newState.polres = hasSelectedPolres;
newState.satker = hasSelectedSatker; newState.satker = hasSelectedSatker;
// Update checkbox "semua" berdasarkan level user // Update checkbox "semua" berdasarkan level user
if (levelNumber === 1) { if (levelNumber === 1) {
// Level 1: semua checkbox harus aktif (nasional, polda, polres, satker, international) // Level 1: semua checkbox harus aktif (nasional, polda, polres, satker, international)
newState.semua = newState.nasional && hasSelectedPolda && hasSelectedPolres && hasSelectedSatker && newState.international; newState.semua =
newState.nasional &&
hasSelectedPolda &&
hasSelectedPolres &&
hasSelectedSatker &&
newState.international;
} else if (levelNumber === 2) { } else if (levelNumber === 2) {
// Level 2: hanya polres yang perlu aktif // Level 2: hanya polres yang perlu aktif
newState.semua = hasSelectedPolres; newState.semua = hasSelectedPolres;
} else { } else {
newState.semua = false; newState.semua = false;
} }
return newState; return newState;
}); });
// Update agendaType berdasarkan checkbox yang aktif // Update agendaType berdasarkan checkbox yang aktif
const selectedKeys = []; const selectedKeys = [];
if (hasSelectedPolda) selectedKeys.push(wilayahValueMap.polda); if (hasSelectedPolda) selectedKeys.push(wilayahValueMap.polda);
if (hasSelectedPolres) selectedKeys.push(wilayahValueMap.polres); if (hasSelectedPolres) selectedKeys.push(wilayahValueMap.polres);
if (hasSelectedSatker) selectedKeys.push(wilayahValueMap.satker); if (hasSelectedSatker) selectedKeys.push(wilayahValueMap.satker);
setAgendaType(selectedKeys.join(",")); setAgendaType(selectedKeys.join(","));
} }
}; };
// Fungsi untuk sinkronisasi checkbox modal dengan Jenis Agenda // Fungsi untuk sinkronisasi checkbox modal dengan Jenis Agenda
const syncModalWithJenisAgenda = () => { const syncModalWithJenisAgenda = () => {
// Hanya jalankan sinkronisasi jika perubahan berasal dari checkbox Jenis Agenda // Hanya jalankan sinkronisasi jika perubahan berasal dari checkbox Jenis Agenda
if (isUpdatingFromJenisAgenda) { if (isUpdatingFromJenisAgenda) {
const newCheckedLevels = new Set(checkedLevels); const newCheckedLevels = new Set(checkedLevels);
// Handle checklist actions - menambahkan semua item ke modal // Handle checklist actions - menambahkan semua item ke modal
if (jenisAgendaChangeType === "polda_checked") { if (jenisAgendaChangeType === "polda_checked") {
// Checklist semua polda // Checklist semua polda
@ -391,7 +415,11 @@ const EventModal = ({
} else if (jenisAgendaChangeType === "polres_checked") { } else if (jenisAgendaChangeType === "polres_checked") {
// Checklist semua polres, tapi hanya yang poldanya sudah di-checklist // Checklist semua polres, tapi hanya yang poldanya sudah di-checklist
listDest.forEach((item: any) => { listDest.forEach((item: any) => {
if (item.levelNumber === 2 && item.name !== "SATKER POLRI" && newCheckedLevels.has(Number(item.id))) { if (
item.levelNumber === 2 &&
item.name !== "SATKER POLRI" &&
newCheckedLevels.has(Number(item.id))
) {
if (item.subDestination) { if (item.subDestination) {
item.subDestination.forEach((polres: any) => { item.subDestination.forEach((polres: any) => {
newCheckedLevels.add(Number(polres.id)); newCheckedLevels.add(Number(polres.id));
@ -401,7 +429,9 @@ const EventModal = ({
}); });
} else if (jenisAgendaChangeType === "satker_checked") { } else if (jenisAgendaChangeType === "satker_checked") {
// Checklist satker // Checklist satker
const satkerItem: any = listDest.find((item: any) => item.name === "SATKER POLRI"); const satkerItem: any = listDest.find(
(item: any) => item.name === "SATKER POLRI"
);
if (satkerItem) { if (satkerItem) {
newCheckedLevels.add(Number(satkerItem.id)); newCheckedLevels.add(Number(satkerItem.id));
if (satkerItem.subDestination) { if (satkerItem.subDestination) {
@ -434,10 +464,12 @@ const EventModal = ({
} }
} }
}); });
setWilayahPublish(prev => ({ ...prev, polres: false })); setWilayahPublish((prev) => ({ ...prev, polres: false }));
} else if (jenisAgendaChangeType === "satker_unchecked") { } else if (jenisAgendaChangeType === "satker_unchecked") {
// Clear satker dari checkedLevels // Clear satker dari checkedLevels
const satkerItem: any = listDest.find((item: any) => item.name === "SATKER POLRI"); const satkerItem: any = listDest.find(
(item: any) => item.name === "SATKER POLRI"
);
if (satkerItem) { if (satkerItem) {
newCheckedLevels.delete(Number(satkerItem.id)); newCheckedLevels.delete(Number(satkerItem.id));
if (satkerItem.subDestination) { if (satkerItem.subDestination) {
@ -447,9 +479,9 @@ const EventModal = ({
} }
} }
} }
setCheckedLevels(newCheckedLevels); setCheckedLevels(newCheckedLevels);
// Reset flag setelah sinkronisasi selesai // Reset flag setelah sinkronisasi selesai
setIsUpdatingFromJenisAgenda(false); setIsUpdatingFromJenisAgenda(false);
setJenisAgendaChangeType(""); setJenisAgendaChangeType("");
@ -484,12 +516,14 @@ const EventModal = ({
setCheckedLevels((prev) => { setCheckedLevels((prev) => {
const updatedLevels = new Set(prev); const updatedLevels = new Set(prev);
const isCurrentlyChecked = updatedLevels.has(levelId); const isCurrentlyChecked = updatedLevels.has(levelId);
if (isCurrentlyChecked) { if (isCurrentlyChecked) {
updatedLevels.delete(levelId); updatedLevels.delete(levelId);
// Jika ini adalah POLDA yang di-unchecklist, unchecklist juga semua polres di bawahnya // Jika ini adalah POLDA yang di-unchecklist, unchecklist juga semua polres di bawahnya
const poldaItem = listDest.find((item: any) => Number(item.id) === levelId) as any; const poldaItem = listDest.find(
(item: any) => Number(item.id) === levelId
) as any;
if (poldaItem && poldaItem.subDestination) { if (poldaItem && poldaItem.subDestination) {
poldaItem.subDestination.forEach((polres: any) => { poldaItem.subDestination.forEach((polres: any) => {
updatedLevels.delete(Number(polres.id)); updatedLevels.delete(Number(polres.id));
@ -514,7 +548,12 @@ const EventModal = ({
const toggleWilayah = (key: string) => { const toggleWilayah = (key: string) => {
// Set flag bahwa perubahan berasal dari checkbox Jenis Agenda // Set flag bahwa perubahan berasal dari checkbox Jenis Agenda
setIsUpdatingFromJenisAgenda(true); setIsUpdatingFromJenisAgenda(true);
setJenisAgendaChangeType(key + (wilayahPublish[key as keyof typeof wilayahPublish] ? "_unchecked" : "_checked")); setJenisAgendaChangeType(
key +
(wilayahPublish[key as keyof typeof wilayahPublish]
? "_unchecked"
: "_checked")
);
setWilayahPublish((prev: any) => { setWilayahPublish((prev: any) => {
let newState = { ...prev }; let newState = { ...prev };
@ -551,18 +590,21 @@ const EventModal = ({
return newState; return newState;
} }
// Validasi khusus untuk POLRES // Validasi khusus untuk POLRES
if (key === "polres" && !prev[key]) { if (key === "polres" && !prev[key]) {
// Cek apakah ada POLDA yang sudah dipilih di modal // Cek apakah ada POLDA yang sudah dipilih di modal
const hasSelectedPolda = listDest.some((item: any) => const hasSelectedPolda = listDest.some(
item.levelNumber === 2 && (item: any) =>
item.name !== "SATKER POLRI" && item.levelNumber === 2 &&
checkedLevels.has(Number(item.id)) item.name !== "SATKER POLRI" &&
checkedLevels.has(Number(item.id))
); );
if (!hasSelectedPolda) { if (!hasSelectedPolda) {
// Jika tidak ada POLDA yang dipilih, tampilkan peringatan dan batalkan // Jika tidak ada POLDA yang dipilih, tampilkan peringatan dan batalkan
alert("Harap pilih POLDA di Modal terlebih dahulu sebelum mengaktifkan checkbox POLRES."); alert(
"Harap pilih POLDA di Modal terlebih dahulu sebelum mengaktifkan checkbox POLRES."
);
// Reset flag karena perubahan dibatalkan // Reset flag karena perubahan dibatalkan
setIsUpdatingFromJenisAgenda(false); setIsUpdatingFromJenisAgenda(false);
setJenisAgendaChangeType(""); setJenisAgendaChangeType("");
@ -598,7 +640,9 @@ const EventModal = ({
}); });
} else if (key === "satker") { } else if (key === "satker") {
// Clear satker // Clear satker
const satkerItem: any = listDest.find((item: any) => item.name === "SATKER POLRI"); const satkerItem: any = listDest.find(
(item: any) => item.name === "SATKER POLRI"
);
if (satkerItem) { if (satkerItem) {
newCheckedLevels.delete(Number(satkerItem.id)); newCheckedLevels.delete(Number(satkerItem.id));
if (satkerItem.subDestination) { if (satkerItem.subDestination) {
@ -613,9 +657,14 @@ const EventModal = ({
// Update checkbox "semua" berdasarkan status semua checkbox lainnya // Update checkbox "semua" berdasarkan status semua checkbox lainnya
// Untuk level 1: semua, nasional, polda, polres, satker, international harus aktif // Untuk level 1: semua, nasional, polda, polres, satker, international harus aktif
// Untuk level 2: semua, polres harus aktif // Untuk level 2: semua, polres harus aktif
if (levelNumber === 1) { if (levelNumber === 1) {
newState.semua = newState.nasional && newState.polda && newState.polres && newState.satker && newState.international; newState.semua =
newState.nasional &&
newState.polda &&
newState.polres &&
newState.satker &&
newState.international;
} else if (levelNumber === 2) { } else if (levelNumber === 2) {
newState.semua = newState.polres; newState.semua = newState.polres;
} else { } else {
@ -770,7 +819,7 @@ const EventModal = ({
const onDeleteEventAction = async () => { const onDeleteEventAction = async () => {
try { try {
} catch (error) { } } catch (error) {}
}; };
const handleOpenDeleteModal = (eventId: string) => { const handleOpenDeleteModal = (eventId: string) => {
@ -950,7 +999,7 @@ const EventModal = ({
); );
}; };
const handleRemoveFile = (id: number) => { }; const handleRemoveFile = (id: number) => {};
async function doDelete(id: any) { async function doDelete(id: any) {
loading(); loading();
@ -1106,40 +1155,42 @@ const EventModal = ({
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label htmlFor="wilayahPublish">Jenis Agenda</Label> <Label htmlFor="wilayahPublish">Jenis Agenda</Label>
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<div>
<Checkbox
id="semua"
checked={wilayahPublish.semua}
onCheckedChange={() => toggleWilayah("semua")}
/>
<label htmlFor="semua" className="ml-2 text-sm">
Semua
</label>
</div>
{levelNumber === 1 && (
<>
<div>
<Checkbox
id="nasional"
checked={wilayahPublish.nasional}
onCheckedChange={() => toggleWilayah("nasional")}
/>
<label htmlFor="nasional" className="ml-2 text-sm mr-2">
Nasional
</label>
</div>
<div> <div>
<Checkbox <Checkbox
id="polda" id="semua"
checked={wilayahPublish.polda} checked={wilayahPublish.semua}
onCheckedChange={() => toggleWilayah("polda")} onCheckedChange={() => toggleWilayah("semua")}
/> />
<label htmlFor="polda" className="mx-2 text-sm mr-2"> <label htmlFor="semua" className="ml-2 text-sm">
Polda Semua
</label> </label>
</div> </div>
{levelNumber === 1 && (
<>
<div>
<Checkbox
id="nasional"
checked={wilayahPublish.nasional}
onCheckedChange={() => toggleWilayah("nasional")}
/>
<label
htmlFor="nasional"
className="ml-2 text-sm mr-2"
>
Nasional
</label>
</div>
<div>
<Checkbox
id="polda"
checked={wilayahPublish.polda}
onCheckedChange={() => toggleWilayah("polda")}
/>
<label htmlFor="polda" className="mx-2 text-sm mr-2">
Polda
</label>
</div>
</> </>
)} )}
@ -1151,39 +1202,41 @@ const EventModal = ({
onCheckedChange={() => toggleWilayah("polres")} onCheckedChange={() => toggleWilayah("polres")}
/> />
<label htmlFor="polres" className="ml-2 text-sm mr-2"> <label htmlFor="polres" className="ml-2 text-sm mr-2">
Polres Polres
</label> </label>
</div> </div>
)} )}
{levelNumber === 1 && ( {levelNumber === 1 && (
<> <>
<div> <div>
<Checkbox <Checkbox
id="satker" id="satker"
checked={wilayahPublish.satker} checked={wilayahPublish.satker}
onCheckedChange={() => toggleWilayah("satker")} onCheckedChange={() => toggleWilayah("satker")}
/> />
<label htmlFor="satker" className="mx-2 text-sm mr-2"> <label htmlFor="satker" className="mx-2 text-sm mr-2">
Satker Satker
</label> </label>
</div> </div>
<div> <div>
<Checkbox <Checkbox
id="international" id="international"
checked={wilayahPublish.international} checked={wilayahPublish.international}
onCheckedChange={() => toggleWilayah("international")} onCheckedChange={() =>
/> toggleWilayah("international")
<label }
htmlFor="international" />
className="ml-2 text-sm mr-2" <label
> htmlFor="international"
Internasional className="ml-2 text-sm mr-2"
</label> >
</div> Internasional
</label>
</div>
</> </>
)} )}
<div className="pl-1"> <div className="pl-1">
<Dialog> <Dialog>
<DialogTrigger asChild> <DialogTrigger asChild>
@ -1202,7 +1255,9 @@ const EventModal = ({
<div key={polda.id} className="border p-2"> <div key={polda.id} className="border p-2">
<Label className="flex items-center"> <Label className="flex items-center">
<Checkbox <Checkbox
checked={checkedLevels.has(Number(polda.id))} checked={checkedLevels.has(
Number(polda.id)
)}
onCheckedChange={() => onCheckedChange={() =>
handleCheckboxChange(Number(polda.id)) handleCheckboxChange(Number(polda.id))
} }
@ -1240,9 +1295,13 @@ const EventModal = ({
polda?.subDestination?.forEach( polda?.subDestination?.forEach(
(polres: any) => { (polres: any) => {
if (isChecked) { if (isChecked) {
updatedLevels.add(Number(polres.id)); updatedLevels.add(
Number(polres.id)
);
} else { } else {
updatedLevels.delete(Number(polres.id)); updatedLevels.delete(
Number(polres.id)
);
} }
} }
); );
@ -1252,18 +1311,27 @@ const EventModal = ({
/> />
Pilih Semua Pilih Semua
</Label> </Label>
{polda?.subDestination?.map((polres: any) => ( {polda?.subDestination?.map(
<Label key={polres.id} className="block mt-1"> (polres: any) => (
<Checkbox <Label
checked={checkedLevels.has(Number(polres.id))} key={polres.id}
onCheckedChange={() => className="block mt-1"
handleCheckboxChange(Number(polres.id)) >
} <Checkbox
className="mr-2" checked={checkedLevels.has(
/> Number(polres.id)
{polres.name} )}
</Label> onCheckedChange={() =>
))} handleCheckboxChange(
Number(polres.id)
)
}
className="mr-2"
/>
{polres.name}
</Label>
)
)}
</div> </div>
)} )}
</div> </div>
@ -1304,8 +1372,7 @@ const EventModal = ({
<Label>Video</Label> <Label>Video</Label>
<FileUploader <FileUploader
accept={{ accept={{
"mp4/*": [], "video/*": [],
"mov/*": [],
}} }}
maxSize={100} maxSize={100}
label="Upload file dengan format .mp4 atau .mov." label="Upload file dengan format .mp4 atau .mov."
@ -1317,7 +1384,7 @@ const EventModal = ({
className="object-fill h-full w-full rounded-md" className="object-fill h-full w-full rounded-md"
src={file.url} src={file.url}
controls controls
title={`Video ${file.id}`} // Mengganti alt dengan title title={`Video ${file.id}`}
/> />
<div <div
key={index} key={index}
@ -1396,10 +1463,9 @@ const EventModal = ({
</div> </div>
<div> <div>
<Label>Teks</Label> <Label>Teks</Label>
<FileUploader <FileUploader
accept={{ accept={{
"pdf/*": [], "application/pdf": [],
}} }}
maxSize={100} maxSize={100}
label="Upload file dengan format .pdf." label="Upload file dengan format .pdf."
@ -1455,8 +1521,7 @@ const EventModal = ({
/> />
<FileUploader <FileUploader
accept={{ accept={{
"mp3/*": [], "audio/*": [],
"wav/*": [],
}} }}
maxSize={100} maxSize={100}
label="Upload file dengan format .mp3 atau .wav." label="Upload file dengan format .mp3 atau .wav."
@ -1482,7 +1547,8 @@ const EventModal = ({
type="button" type="button"
onClick={onPlayPause} onClick={onPlayPause}
disabled={isPlaying} disabled={isPlaying}
className={`flex items-center gap-2 ${isPlaying className={`flex items-center gap-2 ${
isPlaying
? "bg-gray-300 cursor-not-allowed" ? "bg-gray-300 cursor-not-allowed"
: "bg-primary text-white" : "bg-primary text-white"
} p-2 rounded`} } p-2 rounded`}

View File

@ -14,6 +14,7 @@ import Swal from "sweetalert2";
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 { import {
getLocaleTime, getLocaleTime,
@ -24,7 +25,6 @@ import {
detailMediaSummary, detailMediaSummary,
getMediaBlastCampaignList, getMediaBlastCampaignList,
saveMediaBlastBroadcast, saveMediaBlastBroadcast,
saveMediaBlastCampaign,
} from "@/service/broadcast/broadcast"; } from "@/service/broadcast/broadcast";
import { error } from "@/config/swal"; import { error } from "@/config/swal";
import { Link, useRouter } from "@/i18n/routing"; import { Link, useRouter } from "@/i18n/routing";
@ -44,27 +44,16 @@ import {
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
const CustomEditor = dynamic( const CustomEditor = dynamic(
() => { () => import("@/components/editor/custom-editor"),
return import("@/components/editor/custom-editor");
},
{ ssr: false } { ssr: false }
); );
const animatedComponent = makeAnimated(); const animatedComponent = makeAnimated();
// -------------------------
// ZOD SCHEMA (baru)
// -------------------------
const FormSchema = z.object({ const FormSchema = z.object({
title: z.string({
required_error: "Required",
}),
url: z.string({
required_error: "Required",
}),
thumbnail: z.string({
required_error: "Required",
}),
detail: z.string({
required_error: "Required",
}),
selected: z selected: z
.array( .array(
z.object({ z.object({
@ -73,9 +62,16 @@ const FormSchema = z.object({
value: z.string(), value: z.string(),
}) })
) )
.refine((value) => value.length > 0, { .min(1, "Pilih minimal satu campaign"),
message: "Required",
}), title: z.string().min(1, "Required"),
url: z.string().min(1, "Required"),
thumbnail: z.string().min(1, "Required"),
detail: z.string().min(1, "Required"),
messageType: z
.array(z.string())
.min(1, "Pilih minimal satu tipe pesan (WA / Email)"),
}); });
interface Campaign { interface Campaign {
@ -88,66 +84,77 @@ export default function ContentBlast(props: { type: string }) {
const id = useParams()?.id; const id = useParams()?.id;
const MySwal = withReactContent(Swal); const MySwal = withReactContent(Swal);
const router = useRouter(); const router = useRouter();
const { type } = props;
const [dataSelectCampaign, setDataSelectCampaign] = useState<Campaign[]>([]); const [dataSelectCampaign, setDataSelectCampaign] = useState<Campaign[]>([]);
const [openModal, setOpenModal] = useState(false); const [openModal, setOpenModal] = useState(false);
const form = useForm<z.infer<typeof FormSchema>>({ const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema), resolver: zodResolver(FormSchema),
defaultValues: { selected: [], detail: "" }, defaultValues: {
selected: [],
detail: "",
messageType: [], // checkbox
},
}); });
const selectedTypes = form.watch("messageType");
const onSubmit = async (data: z.infer<typeof FormSchema>) => { const onSubmit = async (data: z.infer<typeof FormSchema>) => {
if (form.getValues("detail") == "") { MySwal.fire({
form.setError("detail", { title: "Kirim Broadcast?",
type: "manual", text: "Pesan akan dikirim ke semua campaign yang dipilih",
message: "Required", icon: "warning",
}); showCancelButton: true,
} else { cancelButtonColor: "#d33",
MySwal.fire({ confirmButtonColor: "#3085d6",
title: "Simpan Data", confirmButtonText: "Kirim",
text: "Apakah Anda yakin ingin menyimpan data ini?", }).then((result) => {
icon: "warning", if (result.isConfirmed) save(data);
showCancelButton: true, });
cancelButtonColor: "#d33",
confirmButtonColor: "#3085d6",
confirmButtonText: "Simpan",
}).then((result) => {
if (result.isConfirmed) {
save(data);
}
});
}
}; };
// -------------------------
// SAVE LOGIC (baru)
// -------------------------
const save = async (data: z.infer<typeof FormSchema>) => { const save = async (data: z.infer<typeof FormSchema>) => {
const selectedCampaign = form.getValues("selected"); const selectedCampaign = data.selected;
for (let i = 0; i < selectedCampaign.length; i++) { for (let i = 0; i < selectedCampaign.length; i++) {
const reqData = { const campaignId = selectedCampaign[i].id;
mediaUploadId: id,
mediaBlastCampaignId: selectedCampaign[i].id,
subject: type == "wa" ? `*${data.title}*` : data.title,
body: data.detail?.replace(/\n/g, ""),
type: type,
isScheduled: false,
thumbnail: data?.thumbnail,
sendDate: getLocaleTimestamp(new Date()),
sendTime: getLocaleTime(new Date()),
contentUrl: data.url,
};
console.log("req =>", reqData); // Loop WA / Email
const response = await saveMediaBlastBroadcast(reqData); for (let mt of data.messageType) {
const payload = {
mediaUploadId: id,
mediaBlastCampaignId: campaignId,
subject: mt === "wa" ? `*${data.title}*` : data.title,
body: data.detail.replace(/\n/g, ""),
type: mt, // <-- WA / email
isScheduled: false,
thumbnail: data.thumbnail,
sendDate: getLocaleTimestamp(new Date()),
sendTime: getLocaleTime(new Date()),
contentUrl: data.url,
};
console.log("REQ =>", payload);
await saveMediaBlastBroadcast(payload);
}
} }
setOpenModal(true); setOpenModal(true);
}; };
useEffect(() => { useEffect(() => {
async function initState() { async function init() {
const response = await detailMediaSummary(String(id)); const response = await detailMediaSummary(String(id));
const details = response?.data?.data; const details = response?.data?.data;
if (!details) return;
form.setValue("thumbnail", details.smallThumbnailLink);
form.setValue("title", details.title);
form.setValue("url", details.pageUrl);
let pageUrl = details?.pageUrl || ""; let pageUrl = details?.pageUrl || "";
if (pageUrl.includes("mediahub.polri.go.id")) { if (pageUrl.includes("mediahub.polri.go.id")) {
pageUrl = pageUrl.replace( pageUrl = pageUrl.replace(
@ -157,59 +164,43 @@ export default function ContentBlast(props: { type: string }) {
} }
); );
} }
if (details != undefined) {
form.setValue("thumbnail", details.smallThumbnailLink); const body = `<div><p>Berita hari ini !!!</p>
let body = `<div><p>Berita hari ini !!!</p> <div style='margin-top:20px;border:1px solid;border-radius:10px;width:500px;background-color:#f7f7f7'>
<div style='margin-top:20px;border:1px solid;border-radius:10px;width:500px;background-color:#f7f7f7'> <div>
<div> <img style='width:500px;height:auto;border-radius:10px;object-fit:cover' src='${
<img style='width:500px;height:auto;border-radius:10px;object-fit:cover' src='${ details.smallThumbnailLink
details?.smallThumbnailLink }'>
}'> </div>
</div> <a style='padding:5px 20px;margin:0;text-decoration:none' href='${pageUrl}'>${pageUrl}</a>
<a style='padding:5px 20px;margin:0;text-decoration:none' href='${pageUrl}'>${pageUrl}</a>
<h3 style='padding:5px 20px;margin:0'>${details?.title}</h3> <h3 style='padding:5px 20px;margin:0'>${details.title}</h3>
<p style='padding:0 20px;margin:0;margin-bottom:10px'> <p style='padding:0 20px;margin:0;margin-bottom:10px'>
${textEllipsis(details?.description, 150)} ${textEllipsis(details.description, 150)}
</p> </p>
</div> </div>
</div>`; </div>`;
form.setValue("title", `${details?.title}`);
form.setValue( form.setValue("detail", body);
"url",
details?.pageUrl || "https://mediahub.polri.go.id"
);
if (type == "wa") {
body = `${textEllipsis(details?.description, 150)}`;
form.setValue("detail", body);
} else {
form.setValue("detail", body);
}
}
} }
async function getCampaign() { async function loadCampaign() {
const response = await getMediaBlastCampaignList(); const response = await getMediaBlastCampaignList();
const campaign = response?.data?.data?.content; const campaign = response?.data?.data?.content;
handleLabelCampaign(campaign); handleLabelCampaign(campaign);
console.log(campaign);
} }
initState(); init();
getCampaign(); loadCampaign();
}, [id]); }, [id]);
function handleLabelCampaign(data: any) { function handleLabelCampaign(data: any) {
const optionArr: any = []; const arr = data.map((item: any) => ({
id: item.id,
data.map((option: any) => { label: item.title,
optionArr.push({ value: item.title,
id: option.id, }));
label: option.title, setDataSelectCampaign(arr);
value: option.title,
});
});
console.log("option arr", optionArr);
setDataSelectCampaign(optionArr);
} }
return ( return (
@ -218,104 +209,127 @@ export default function ContentBlast(props: { type: string }) {
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}
className="space-y-3 bg-white rounded-sm p-4" className="space-y-3 bg-white rounded-sm p-4"
> >
<p className="fonnt-semibold">Broadcast</p> <p className="font-semibold">Broadcast</p>
{/* SELECT CAMPAIGN */}
<FormField <FormField
control={form.control} control={form.control}
name="selected" name="selected"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Subject</FormLabel> <FormLabel>Pilih Campaign</FormLabel>
<Select <Select
className="z-50"
options={dataSelectCampaign} options={dataSelectCampaign}
closeMenuOnSelect={false} closeMenuOnSelect={false}
components={animatedComponent} components={animatedComponent}
onChange={field.onChange}
isMulti isMulti
onChange={field.onChange}
/> />
<FormMessage />
</FormItem>
)}
/>
{/* CHECKBOX MESSAGE TYPE */}
<FormField
control={form.control}
name="messageType"
render={({ field }) => (
<FormItem>
<FormLabel>Kirim sebagai:</FormLabel>
<div className="flex gap-6">
<label className="flex items-center gap-2">
<Checkbox
checked={field.value.includes("wa")}
onCheckedChange={(checked) => {
checked
? field.onChange([...field.value, "wa"])
: field.onChange(field.value.filter((v) => v !== "wa"));
}}
/>
WhatsApp
</label>
<label className="flex items-center gap-2">
<Checkbox
checked={field.value.includes("email")}
onCheckedChange={(checked) => {
checked
? field.onChange([...field.value, "email"])
: field.onChange(
field.value.filter((v) => v !== "email")
);
}}
/>
Email
</label>
</div>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
{/* TITLE */}
<FormField <FormField
control={form.control} control={form.control}
name="title" name="title"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Subject</FormLabel> <FormLabel>Judul</FormLabel>
<Input <Input {...field} placeholder="Masukkan judul" />
value={field.value}
placeholder="Masukkan Judul"
onChange={field.onChange}
/>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
{/* DETAIL */}
<FormField <FormField
control={form.control} control={form.control}
name="detail" name="detail"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Detail Perencanaan</FormLabel> <FormLabel>Isi Pesan</FormLabel>
{type === "wa" ? (
<Textarea value={field.value} onChange={field.onChange} /> <CustomEditor
) : ( onChange={field.onChange}
<CustomEditor initialData={field.value}
onChange={field.onChange} />
initialData={field.value}
/>
)}
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<div className="flex flex-row gap-2 mt-4 pt-4">
<Button {/* BUTTONS */}
size="md" <div className="flex gap-2 mt-4">
type="button" <Button variant="outline" type="button">
variant="outline"
color="destructive"
className="text-xs"
>
Cancel Cancel
</Button> </Button>
<Button size="md" type="submit" color="primary" className="text-xs"> <Button type="submit" color="primary">
Submit Submit
</Button> </Button>
</div> </div>
{/* MODAL SENT */}
<Dialog open={openModal}> <Dialog open={openModal}>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>Terkirim !!</DialogTitle> <DialogTitle>Terkirim!</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="flex flex-col justify-center items-center gap-3 mb-3 text-sm">
<div className="flex flex-col items-center gap-3">
<img <img
src="/assets/img/illust-for-broadcast-sent.png" src="/assets/img/illust-for-broadcast-sent.png"
className="w-[70%]" className="w-[70%]"
/> />
Untuk melihat Email Terkirim silahkan cek menu Sent! Pesan telah dikirim sesuai pilihan Anda.
</div> </div>
<DialogFooter className="flex justify-center">
{/* <Link
href={`/admin/broadcast/campaign-list/detail/${
form.getValues("selected")[0]?.id
}`}
>
<Button type="button" color="success">
Menu "Sent"
</Button>
</Link> */}
<Button <DialogFooter className="flex justify-center">
type="button" <Button onClick={() => router.push("/admin/broadcast")}>
onClick={() => router.push("/admin/broadcast")} OK
color="primary"
>
Okay
</Button> </Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
@ -324,3 +338,330 @@ export default function ContentBlast(props: { type: string }) {
</Form> </Form>
); );
} }
// "use client";
// import { z } from "zod";
// import { useForm } from "react-hook-form";
// import { zodResolver } from "@hookform/resolvers/zod";
// import {
// Form,
// FormField,
// FormItem,
// FormLabel,
// FormMessage,
// } from "@/components/ui/form";
// import withReactContent from "sweetalert2-react-content";
// import Swal from "sweetalert2";
// import { Input } from "@/components/ui/input";
// import { Button } from "@/components/ui/button";
// import {
// getLocaleTime,
// getLocaleTimestamp,
// textEllipsis,
// } from "@/utils/globals";
// import {
// detailMediaSummary,
// getMediaBlastCampaignList,
// saveMediaBlastBroadcast,
// saveMediaBlastCampaign,
// } from "@/service/broadcast/broadcast";
// import { error } from "@/config/swal";
// import { Link, useRouter } from "@/i18n/routing";
// import { useEffect, useRef, useState } from "react";
// import { useParams } from "next/navigation";
// import Select from "react-select";
// import makeAnimated from "react-select/animated";
// import { Textarea } from "@/components/ui/textarea";
// import {
// Dialog,
// DialogContent,
// DialogFooter,
// DialogHeader,
// DialogTitle,
// } from "@/components/ui/dialog";
// import dynamic from "next/dynamic";
// const CustomEditor = dynamic(
// () => {
// return import("@/components/editor/custom-editor");
// },
// { ssr: false }
// );
// const animatedComponent = makeAnimated();
// const FormSchema = z.object({
// title: z.string({
// required_error: "Required",
// }),
// url: z.string({
// required_error: "Required",
// }),
// thumbnail: z.string({
// required_error: "Required",
// }),
// detail: z.string({
// required_error: "Required",
// }),
// selected: z
// .array(
// z.object({
// id: z.number(),
// label: z.string(),
// value: z.string(),
// })
// )
// .refine((value) => value.length > 0, {
// message: "Required",
// }),
// });
// interface Campaign {
// id: string;
// name: string;
// }
// export default function ContentBlast(props: { type: string }) {
// const editor = useRef(null);
// const id = useParams()?.id;
// const MySwal = withReactContent(Swal);
// const router = useRouter();
// const { type } = props;
// const [dataSelectCampaign, setDataSelectCampaign] = useState<Campaign[]>([]);
// const [openModal, setOpenModal] = useState(false);
// const form = useForm<z.infer<typeof FormSchema>>({
// resolver: zodResolver(FormSchema),
// defaultValues: { selected: [], detail: "" },
// });
// const onSubmit = async (data: z.infer<typeof FormSchema>) => {
// if (form.getValues("detail") == "") {
// form.setError("detail", {
// type: "manual",
// message: "Required",
// });
// } else {
// MySwal.fire({
// title: "Simpan Data",
// text: "Apakah Anda yakin ingin menyimpan data ini?",
// icon: "warning",
// showCancelButton: true,
// cancelButtonColor: "#d33",
// confirmButtonColor: "#3085d6",
// confirmButtonText: "Simpan",
// }).then((result) => {
// if (result.isConfirmed) {
// save(data);
// }
// });
// }
// };
// const save = async (data: z.infer<typeof FormSchema>) => {
// const selectedCampaign = form.getValues("selected");
// for (let i = 0; i < selectedCampaign.length; i++) {
// const reqData = {
// mediaUploadId: id,
// mediaBlastCampaignId: selectedCampaign[i].id,
// subject: type == "wa" ? `*${data.title}*` : data.title,
// body: data.detail?.replace(/\n/g, ""),
// type: type,
// isScheduled: false,
// thumbnail: data?.thumbnail,
// sendDate: getLocaleTimestamp(new Date()),
// sendTime: getLocaleTime(new Date()),
// contentUrl: data.url,
// };
// console.log("req =>", reqData);
// const response = await saveMediaBlastBroadcast(reqData);
// }
// setOpenModal(true);
// };
// useEffect(() => {
// async function initState() {
// const response = await detailMediaSummary(String(id));
// const details = response?.data?.data;
// let pageUrl = details?.pageUrl || "";
// if (pageUrl.includes("mediahub.polri.go.id")) {
// pageUrl = pageUrl.replace(
// /(\.id)(\/|$)/,
// (match: any, p1: any, p2: any) => {
// return p2.startsWith("/in") ? match : `${p1}/in${p2}`;
// }
// );
// }
// if (details != undefined) {
// form.setValue("thumbnail", details.smallThumbnailLink);
// let body = `<div><p>Berita hari ini !!!</p>
// <div style='margin-top:20px;border:1px solid;border-radius:10px;width:500px;background-color:#f7f7f7'>
// <div>
// <img style='width:500px;height:auto;border-radius:10px;object-fit:cover' src='${
// details?.smallThumbnailLink
// }'>
// </div>
// <a style='padding:5px 20px;margin:0;text-decoration:none' href='${pageUrl}'>${pageUrl}</a>
// <h3 style='padding:5px 20px;margin:0'>${details?.title}</h3>
// <p style='padding:0 20px;margin:0;margin-bottom:10px'>
// ${textEllipsis(details?.description, 150)}
// </p>
// </div>
// </div>`;
// form.setValue("title", `${details?.title}`);
// form.setValue(
// "url",
// details?.pageUrl || "https://mediahub.polri.go.id"
// );
// if (type == "wa") {
// body = `${textEllipsis(details?.description, 150)}`;
// form.setValue("detail", body);
// } else {
// form.setValue("detail", body);
// }
// }
// }
// async function getCampaign() {
// const response = await getMediaBlastCampaignList();
// const campaign = response?.data?.data?.content;
// handleLabelCampaign(campaign);
// console.log(campaign);
// }
// initState();
// getCampaign();
// }, [id]);
// function handleLabelCampaign(data: any) {
// const optionArr: any = [];
// data.map((option: any) => {
// optionArr.push({
// id: option.id,
// label: option.title,
// value: option.title,
// });
// });
// console.log("option arr", optionArr);
// setDataSelectCampaign(optionArr);
// }
// return (
// <Form {...form}>
// <form
// onSubmit={form.handleSubmit(onSubmit)}
// className="space-y-3 bg-white rounded-sm p-4"
// >
// <p className="fonnt-semibold">Broadcast</p>
// <FormField
// control={form.control}
// name="selected"
// render={({ field }) => (
// <FormItem>
// <FormLabel>Subject</FormLabel>
// <Select
// className="z-50"
// options={dataSelectCampaign}
// closeMenuOnSelect={false}
// components={animatedComponent}
// onChange={field.onChange}
// isMulti
// />
// <FormMessage />
// </FormItem>
// )}
// />
// <FormField
// control={form.control}
// name="title"
// render={({ field }) => (
// <FormItem>
// <FormLabel>Subject</FormLabel>
// <Input
// value={field.value}
// placeholder="Masukkan Judul"
// onChange={field.onChange}
// />
// <FormMessage />
// </FormItem>
// )}
// />
// <FormField
// control={form.control}
// name="detail"
// render={({ field }) => (
// <FormItem>
// <FormLabel>Detail Perencanaan</FormLabel>
// {type === "wa" ? (
// <Textarea value={field.value} onChange={field.onChange} />
// ) : (
// <CustomEditor
// onChange={field.onChange}
// initialData={field.value}
// />
// )}
// <FormMessage />
// </FormItem>
// )}
// />
// <div className="flex flex-row gap-2 mt-4 pt-4">
// <Button
// size="md"
// type="button"
// variant="outline"
// color="destructive"
// className="text-xs"
// >
// Cancel
// </Button>
// <Button size="md" type="submit" color="primary" className="text-xs">
// Submit
// </Button>
// </div>
// <Dialog open={openModal}>
// <DialogContent>
// <DialogHeader>
// <DialogTitle>Terkirim !!</DialogTitle>
// </DialogHeader>
// <div className="flex flex-col justify-center items-center gap-3 mb-3 text-sm">
// <img
// src="/assets/img/illust-for-broadcast-sent.png"
// className="w-[70%]"
// />
// Untuk melihat Email Terkirim silahkan cek menu “Sent”!
// </div>
// <DialogFooter className="flex justify-center">
// {/* <Link
// href={`/admin/broadcast/campaign-list/detail/${
// form.getValues("selected")[0]?.id
// }`}
// >
// <Button type="button" color="success">
// Menu "Sent"
// </Button>
// </Link> */}
// <Button
// type="button"
// onClick={() => router.push("/admin/broadcast")}
// color="primary"
// >
// Okay
// </Button>
// </DialogFooter>
// </DialogContent>
// </Dialog>
// </form>
// </Form>
// );
// }

View File

@ -102,6 +102,7 @@ export type taskDetail = {
is_active: string; is_active: string;
isAssignmentAccepted: boolean; isAssignmentAccepted: boolean;
isDone: any; isDone: any;
isAccept?: boolean;
}; };
interface ListData { interface ListData {
@ -244,18 +245,6 @@ export default function FormTaskTaDetail() {
const [selectedCompetencies, setSelectedCompetencies] = useState<Set<number>>( const [selectedCompetencies, setSelectedCompetencies] = useState<Set<number>>(
new Set() new Set()
); );
const [totalPage, setTotalPage] = React.useState(1);
const [dataTable, setDataTable] = React.useState<any[]>([]);
const [totalData, setTotalData] = React.useState<number>(1);
const [imageFiles, setImageFiles] = useState<FileWithPreview[]>([]);
const [videoFiles, setVideoFiles] = useState<FileWithPreview[]>([]);
const [textFiles, setTextFiles] = useState<FileWithPreview[]>([]);
const [audioFiles, setAudioFiles] = useState<FileWithPreview[]>([]);
const [isImageUploadFinish, setIsImageUploadFinish] = useState(false);
const [isVideoUploadFinish, setIsVideoUploadFinish] = useState(false);
const [isTextUploadFinish, setIsTextUploadFinish] = useState(false);
const [isAudioUploadFinish, setIsAudioUploadFinish] = useState(false);
const [voiceNoteLink, setVoiceNoteLink] = useState("");
const [statusAcceptance, setStatusAcceptance] = useState(); const [statusAcceptance, setStatusAcceptance] = useState();
const [modalType, setModalType] = useState<"terkirim" | "diterima" | "">(""); const [modalType, setModalType] = useState<"terkirim" | "diterima" | "">("");
const [Isloading, setLoading] = useState<boolean>(false); const [Isloading, setLoading] = useState<boolean>(false);
@ -265,10 +254,8 @@ export default function FormTaskTaDetail() {
const [isRecording, setIsRecording] = useState(false); const [isRecording, setIsRecording] = useState(false);
const [timer, setTimer] = useState<number>(120); const [timer, setTimer] = useState<number>(120);
const [search, setSearch] = React.useState<string>(""); const [search, setSearch] = React.useState<string>("");
const [wavesurfer, setWavesurfer] = useState<WaveSurfer>(); const [wavesurfer, setWavesurfer] = useState<WaveSurfer>();
const [isPlaying, setIsPlaying] = useState(false); const [isPlaying, setIsPlaying] = useState(false);
const [imageUploadedFiles, setImageUploadedFiles] = useState<FileUploaded[]>( const [imageUploadedFiles, setImageUploadedFiles] = useState<FileUploaded[]>(
[] []
); );
@ -293,6 +280,8 @@ export default function FormTaskTaDetail() {
const [selectedAudio, setSelectedAudio] = useState<string | null>( const [selectedAudio, setSelectedAudio] = useState<string | null>(
audioUploadedFiles?.[0]?.url || null audioUploadedFiles?.[0]?.url || null
); );
const [acceptLoading, setAcceptLoading] = useState(false);
const [isAccepted, setIsAccepted] = useState(false);
const [platformTypeVisible, setPlatformTypeVisible] = useState(false); const [platformTypeVisible, setPlatformTypeVisible] = useState(false);
const [unitSelection, setUnitSelection] = useState({ const [unitSelection, setUnitSelection] = useState({
@ -400,6 +389,18 @@ export default function FormTaskTaDetail() {
fetchListExpert(); fetchListExpert();
}, []); }, []);
useEffect(() => {
async function fetchAcceptanceStatus() {
const res = await getAcceptanceAssignmentStatus(id);
// API: /assignment/acceptance?id=103&isAccept=true
const status = res?.data?.data?.isAccept === true;
setIsAccepted(status);
}
fetchAcceptanceStatus();
}, [id]);
useEffect(() => { useEffect(() => {
const fetchExpertsForCompetencies = async () => { const fetchExpertsForCompetencies = async () => {
const allExperts: any[] = []; const allExperts: any[] = [];
@ -743,18 +744,26 @@ export default function FormTaskTaDetail() {
} }
const handleAcceptAcceptance = async () => { const handleAcceptAcceptance = async () => {
loading(); if (acceptLoading || isAccepted) return;
console.log("Id user :", userId);
const response = await acceptAssignmentTa(id);
if (response?.error) { setAcceptLoading(true);
error(response?.message);
return false; loading();
const res = await acceptAssignmentTa(id);
close();
setAcceptLoading(false);
if (res?.error) {
error(res?.message || "Gagal menerima tugas");
return;
} }
// ini penting!
setIsAccepted(true);
successCallback(); successCallback();
getDataAcceptance();
return false;
}; };
async function getListAcceptance(): Promise<void> { async function getListAcceptance(): Promise<void> {
@ -970,7 +979,9 @@ export default function FormTaskTaDetail() {
{detail !== undefined ? ( {detail !== undefined ? (
<div className="px-6 py-6"> <div className="px-6 py-6">
<div className="flex flex-col sm:flex-row lg:flex-row justify-between"> <div className="flex flex-col sm:flex-row lg:flex-row justify-between">
<p className="text-lg font-semibold mb-3">{t("detail-task", { defaultValue: "Detail Task" })}</p> <p className="text-lg font-semibold mb-3">
{t("detail-task", { defaultValue: "Detail Task" })}
</p>
{/* <div {/* <div
className="flex gap-3" className="flex gap-3"
style={ style={
@ -1020,7 +1031,9 @@ export default function FormTaskTaDetail() {
<form> <form>
<div className="gap-5 mb-5"> <div className="gap-5 mb-5">
<div className="space-y-2"> <div className="space-y-2">
<Label>{t("unique-code", { defaultValue: "Unique Code" })}</Label> <Label>
{t("unique-code", { defaultValue: "Unique Code" })}
</Label>
<Controller <Controller
control={control} control={control}
name="uniqueCode" name="uniqueCode"
@ -1072,7 +1085,9 @@ export default function FormTaskTaDetail() {
</RadioGroup> </RadioGroup>
</div> */} </div> */}
<div className="mt-5 space-y-2"> <div className="mt-5 space-y-2">
<Label>{t("areas-expertise", { defaultValue: "Areas Expertise" })}</Label> <Label>
{t("areas-expertise", { defaultValue: "Areas Expertise" })}
</Label>
<div className="flex flex-wrap gap-4"> <div className="flex flex-wrap gap-4">
{userCompetencies?.map((item: any) => ( {userCompetencies?.map((item: any) => (
<div className="flex items-center gap-2" key={item.id}> <div className="flex items-center gap-2" key={item.id}>
@ -1171,7 +1186,9 @@ export default function FormTaskTaDetail() {
<div></div> <div></div>
<div className="mt-6 space-y-2"> <div className="mt-6 space-y-2">
<Label>{t("description", { defaultValue: "Description" })}</Label> <Label>
{t("description", { defaultValue: "Description" })}
</Label>
<Controller <Controller
control={control} control={control}
name="naration" name="naration"
@ -1186,11 +1203,15 @@ export default function FormTaskTaDetail() {
)} */} )} */}
</div> </div>
<div className=" mt-5 space-y-2"> <div className=" mt-5 space-y-2">
<Label htmlFor="attachment">{t("attachment", { defaultValue: "Attachment" })}</Label> <Label htmlFor="attachment">
{t("attachment", { defaultValue: "Attachment" })}
</Label>
<div className="space-y-3"> <div className="space-y-3">
<div> <div>
{videoUploadedFiles?.length > 0 && ( {videoUploadedFiles?.length > 0 && (
<Label>{t("audio-visual", { defaultValue: "Audio Visual" })}</Label> <Label>
{t("audio-visual", { defaultValue: "Audio Visual" })}
</Label>
)} )}
<div> <div>
{selectedVideo && ( {selectedVideo && (
@ -1466,7 +1487,7 @@ export default function FormTaskTaDetail() {
</Button> </Button>
</div> */} </div> */}
<div className=""> <div className="">
<Button {/* <Button
type="button" type="button"
className="btn btn-primary lg:mx-3" className="btn btn-primary lg:mx-3"
style={ style={
@ -1481,6 +1502,21 @@ export default function FormTaskTaDetail() {
onClick={() => handleAcceptAcceptance()} onClick={() => handleAcceptAcceptance()}
> >
Terima Tugas Terima Tugas
</Button> */}
<Button
type="button"
onClick={handleAcceptAcceptance}
disabled={isAccepted || acceptLoading}
className="
btn btn-primary lg:mx-3
disabled:opacity-50 disabled:cursor-not-allowed
"
>
{isAccepted
? "Tugas Sudah Diterima"
: acceptLoading
? "Memproses..."
: "Terima Tugas"}
</Button> </Button>
</div> </div>
<div <div

View File

@ -514,7 +514,9 @@ export default function FormTaskTa() {
return ( return (
<Card> <Card>
<div className="px-6 py-6"> <div className="px-6 py-6">
<p className="text-lg font-semibold mb-3">{t("form-task", { defaultValue: "Form Task" })}</p> <p className="text-lg font-semibold mb-3">
{t("form-task", { defaultValue: "Form Task" })}
</p>
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)}>
<div className="gap-5 mb-5"> <div className="gap-5 mb-5">
@ -540,7 +542,9 @@ export default function FormTaskTa() {
</div> </div>
<div className="mt-5 space-y-2"> <div className="mt-5 space-y-2">
<Label>{t("assigment-type", { defaultValue: "Assigment Type" })} </Label> <Label>
{t("assigment-type", { defaultValue: "Assigment Type" })}{" "}
</Label>
<RadioGroup <RadioGroup
value={taskType} value={taskType}
onValueChange={(value) => setTaskType(String(value))} onValueChange={(value) => setTaskType(String(value))}
@ -593,7 +597,9 @@ export default function FormTaskTa() {
</Popover> </Popover>
</div> </div>
<div className="mt-5 space-y-2"> <div className="mt-5 space-y-2">
<Label>{t("areas-expertise", { defaultValue: "Areas Expertise" })}</Label> <Label>
{t("areas-expertise", { defaultValue: "Areas Expertise" })}
</Label>
<div className="flex flex-wrap gap-4"> <div className="flex flex-wrap gap-4">
{userCompetencies?.map((item: any) => ( {userCompetencies?.map((item: any) => (
<div className="flex items-center gap-2" key={item.id}> <div className="flex items-center gap-2" key={item.id}>
@ -608,7 +614,9 @@ export default function FormTaskTa() {
</div> </div>
</div> </div>
<div className="mt-5 space-y-2"> <div className="mt-5 space-y-2">
<Label>{t("choose-expert", { defaultValue: "Choose Expert" })}</Label> <Label>
{t("choose-expert", { defaultValue: "Choose Expert" })}
</Label>
<div className="flex flex-wrap gap-4"> <div className="flex flex-wrap gap-4">
<Dialog> <Dialog>
<DialogTrigger asChild> <DialogTrigger asChild>
@ -699,11 +707,23 @@ export default function FormTaskTa() {
)} )}
</div> </div>
<div className="space-y-2.5 mt-5"> <div className="space-y-2.5 mt-5">
<Label htmlFor="attachments">{t("attachment", { defaultValue: "Attachment" })}</Label> <Label htmlFor="attachments">
{t("attachment", { defaultValue: "Attachment" })}
</Label>
<div className="space-y-3"> <div className="space-y-3">
<div> <div>
<Label>{t("audio-visual", { defaultValue: "Audio Visual" })}</Label> <Label>
{t("audio-visual", { defaultValue: "Audio Visual" })}
</Label>
<FileUploader <FileUploader
accept={{
"video/*": [],
}}
maxSize={100}
label="Upload file dengan format video."
onDrop={(files) => setVideoFiles(files)}
/>
{/* <FileUploader
accept={{ accept={{
"mp4/*": [], "mp4/*": [],
"mov/*": [], "mov/*": [],
@ -711,7 +731,7 @@ export default function FormTaskTa() {
maxSize={100} maxSize={100}
label="Upload file dengan format .mp4 atau .mov." label="Upload file dengan format .mp4 atau .mov."
onDrop={(files) => setVideoFiles(files)} onDrop={(files) => setVideoFiles(files)}
/> /> */}
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label>{t("image", { defaultValue: "Image" })}</Label> <Label>{t("image", { defaultValue: "Image" })}</Label>
@ -727,13 +747,21 @@ export default function FormTaskTa() {
<div className="space-y-2"> <div className="space-y-2">
<Label>{t("text", { defaultValue: "Text" })}</Label> <Label>{t("text", { defaultValue: "Text" })}</Label>
<FileUploader <FileUploader
accept={{
"application/pdf": [],
}}
maxSize={100}
label="Upload file PDF."
onDrop={(files) => setTextFiles(files)}
/>
{/* <FileUploader
accept={{ accept={{
"pdf/*": [], "pdf/*": [],
}} }}
maxSize={100} maxSize={100}
label="Upload file dengan format .pdf." label="Upload file dengan format .pdf."
onDrop={(files) => setTextFiles(files)} onDrop={(files) => setTextFiles(files)}
/> /> */}
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label>{t("audio", { defaultValue: "Audio" })}</Label> <Label>{t("audio", { defaultValue: "Audio" })}</Label>
@ -747,6 +775,17 @@ export default function FormTaskTa() {
downloadFileExtension="webm" downloadFileExtension="webm"
/> />
<FileUploader <FileUploader
accept={{
"audio/*": [],
}}
maxSize={100}
label="Upload file audio (mp3, wav, webm)."
onDrop={(files) =>
setAudioFiles((prev) => [...prev, ...files])
}
className="mt-2"
/>
{/* <FileUploader
accept={{ accept={{
"mp3/*": [], "mp3/*": [],
"wav/*": [], "wav/*": [],
@ -757,7 +796,7 @@ export default function FormTaskTa() {
setAudioFiles((prev) => [...prev, ...files]) setAudioFiles((prev) => [...prev, ...files])
} }
className="mt-2" className="mt-2"
/> /> */}
</div> </div>
{audioFiles?.map((audio: any, idx: any) => ( {audioFiles?.map((audio: any, idx: any) => (
<div <div
@ -775,9 +814,11 @@ export default function FormTaskTa() {
</Button> </Button>
</div> </div>
))} ))}
{/* {isRecording && <p>Recording... {timer} seconds remaining</p>}{" "} {isRecording && <p>Recording... {timer} seconds remaining</p>}{" "}
<div className="mt-4 space-y-2"> <div className="mt-4 space-y-2">
<Label className="">{t("news-links", { defaultValue: "News Links" })}</Label> <Label className="">
{t("news-links", { defaultValue: "News Links" })}
</Label>
{links.map((link, index) => ( {links.map((link, index) => (
<div key={index} className="flex items-center gap-2 mt-2"> <div key={index} className="flex items-center gap-2 mt-2">
<Input <Input
@ -808,7 +849,7 @@ export default function FormTaskTa() {
> >
{t("add-links", { defaultValue: "Add Links" })} {t("add-links", { defaultValue: "Add Links" })}
</Button> </Button>
</div> */} </div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -87,9 +87,7 @@ const CustomEditor = dynamic(
export default function FormTaskTaNew() { export default function FormTaskTaNew() {
const MySwal = withReactContent(Swal); const MySwal = withReactContent(Swal);
const router = useRouter(); const router = useRouter();
const editor = useRef(null);
type ImageSchema = z.infer<typeof imageSchema>; type ImageSchema = z.infer<typeof imageSchema>;
const t = useTranslations("Form"); const t = useTranslations("Form");
const [selectedFiles, setSelectedFiles] = useState<File[]>([]); const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
const taskId = Cookies.get("taskId"); const taskId = Cookies.get("taskId");
@ -97,33 +95,14 @@ export default function FormTaskTaNew() {
const scheduleType = Cookies.get("scheduleType"); const scheduleType = Cookies.get("scheduleType");
const roleId = getCookiesDecrypt("urie"); const roleId = getCookiesDecrypt("urie");
const { id } = useParams() as { id: string }; const { id } = useParams() as { id: string };
console.log(id);
const [categories, setCategories] = useState<Category[]>([]); const [categories, setCategories] = useState<Category[]>([]);
const [selectedCategory, setSelectedCategory] = useState<any>(); const [selectedCategory, setSelectedCategory] = useState<any>();
const [tags, setTags] = useState<any[]>([]); const [tags, setTags] = useState<any[]>([]);
const [thumbnail, setThumbnail] = useState<File | null>(null); const [thumbnail, setThumbnail] = useState<File | null>(null);
const [preview, setPreview] = useState<string | null>(null); const [preview, setPreview] = useState<string | null>(null);
const [selectedLanguage, setSelectedLanguage] = useState("");
const [selectedSEO, setSelectedSEO] = useState<string>("");
const [title, setTitle] = useState<string>(""); const [title, setTitle] = useState<string>("");
const [selectedAdvConfig, setSelectedAdvConfig] = useState<string>("");
const [editingArticleId, setEditingArticleId] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [isLoadingData, setIsLoadingData] = useState<boolean>(false); const [isLoadingData, setIsLoadingData] = useState<boolean>(false);
const [articleIds, setArticleIds] = useState<string[]>([]);
const [isGeneratedArticle, setIsGeneratedArticle] = useState(false);
const [articleBody, setArticleBody] = useState<string>(""); const [articleBody, setArticleBody] = useState<string>("");
const [selectedArticleId, setSelectedArticleId] = useState<string | null>(
null
);
const [selectedMainKeyword, setSelectedMainKeyword] = useState("");
const [selectedWritingStyle, setSelectedWritingStyle] = useState("");
const [selectedSize, setSelectedSize] = useState("");
const [detailData, setDetailData] = useState<any>(null);
const [articleImages, setArticleImages] = useState<string[]>([]);
const [isSwitchOn, setIsSwitchOn] = useState<boolean>(false); const [isSwitchOn, setIsSwitchOn] = useState<boolean>(false);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const [imageFiles, setImageFiles] = useState<FileWithPreview[]>([]); const [imageFiles, setImageFiles] = useState<FileWithPreview[]>([]);
@ -139,26 +118,7 @@ export default function FormTaskTaNew() {
const [links, setLinks] = useState<string[]>([""]); const [links, setLinks] = useState<string[]>([""]);
const [timer, setTimer] = useState<number>(120); const [timer, setTimer] = useState<number>(120);
const [fileTypeId, setFileTypeId] = useState<string>(""); const [fileTypeId, setFileTypeId] = useState<string>("");
const [content, setContent] = useState("");
const [selectedTarget, setSelectedTarget] = useState("");
const [unitSelection, setUnitSelection] = useState({
allUnit: false,
mabes: false,
polda: false,
polres: false,
});
let progressInfo: any = [];
let counterUpdateProgress = 0;
const [progressList, setProgressList] = useState<any>([]);
let uploadPersen = 0;
const [isStartUpload, setIsStartUpload] = useState(false);
const [counterProgress, setCounterProgress] = useState(0);
const [files, setFiles] = useState<FileWithPreview[]>([]); const [files, setFiles] = useState<FileWithPreview[]>([]);
const [filesTemp, setFilesTemp] = useState<File[]>([]);
const [publishedFor, setPublishedFor] = useState<string[]>([]); const [publishedFor, setPublishedFor] = useState<string[]>([]);
const [detail, setDetail] = useState<taskDetail>(); const [detail, setDetail] = useState<taskDetail>();
const [refresh] = useState(false); const [refresh] = useState(false);
@ -178,7 +138,7 @@ export default function FormTaskTaNew() {
}); });
const imageSchema = z.object({ const imageSchema = z.object({
title: z.string().min(1, { message: "Judul diperlukan" }), title: z.string().optional(),
description: z description: z
.string() .string()
.min(2, { message: "Narasi Penugasan harus lebih dari 2 karakter." }) .min(2, { message: "Narasi Penugasan harus lebih dari 2 karakter." })
@ -339,7 +299,7 @@ export default function FormTaskTaNew() {
const save = async (data: ImageSchema) => { const save = async (data: ImageSchema) => {
loading(); loading();
const finalTags = tags.join(", "); const finalTags = tags.join(", ");
const finalTitle = isSwitchOn ? title : data.title; const finalTitle = data.title || detail?.title || "";
const finalDescription = articleBody || data.description; const finalDescription = articleBody || data.description;
if (!finalDescription.trim()) { if (!finalDescription.trim()) {
@ -662,8 +622,7 @@ export default function FormTaskTaNew() {
size="md" size="md"
type="text" type="text"
defaultValue={detail?.title} defaultValue={detail?.title}
onChange={field.onChange} {...field}
placeholder="Enter Title"
/> />
)} )}
/> />
@ -676,7 +635,11 @@ export default function FormTaskTaNew() {
<div className="flex flex-col sm:flex-row lg:flex-row sm:items-center lg:items-center gap-3"> <div className="flex flex-col sm:flex-row lg:flex-row sm:items-center lg:items-center gap-3">
<div className=" space-y-2 w-3/12"> <div className=" space-y-2 w-3/12">
<Label>{t("assigment-type", { defaultValue: "Assigment Type" })}</Label> <Label>
{t("assigment-type", {
defaultValue: "Assigment Type",
})}
</Label>
<Select onValueChange={(val) => setFileTypeId(val)}> <Select onValueChange={(val) => setFileTypeId(val)}>
<SelectTrigger size="md"> <SelectTrigger size="md">
<SelectValue placeholder="Choose" /> <SelectValue placeholder="Choose" />
@ -717,7 +680,9 @@ export default function FormTaskTaNew() {
</div> </div>
<div className="py-3 space-y-2"> <div className="py-3 space-y-2">
<Label>{t("description", { defaultValue: "Description" })}</Label> <Label>
{t("description", { defaultValue: "Description" })}
</Label>
<Controller <Controller
control={control} control={control}
name="description" name="description"
@ -743,13 +708,21 @@ export default function FormTaskTaNew() {
)} )}
</div> </div>
<div className="space-y-2.5 mt-5"> <div className="space-y-2.5 mt-5">
<Label htmlFor="attachments">{t("attachment", { defaultValue: "Attachment" })}</Label> <Label htmlFor="attachments">
{t("attachment", { defaultValue: "Attachment" })}
</Label>
<div className="space-y-3"> <div className="space-y-3">
{fileTypeId === "2" && ( {fileTypeId === "2" && (
<div> <div>
<Label>{t("audio-visual", { defaultValue: "Audio Visual" })}</Label> <Label>
{t("audio-visual", {
defaultValue: "Audio Visual",
})}
</Label>
<FileUploader <FileUploader
accept={{ "mp4/*": [], "mov/*": [] }} accept={{
"video/*": [], // menerima mp4, mov, mkv, avi, dll (lebih aman)
}}
maxSize={100} maxSize={100}
label="Upload file dengan format .mp4 atau .mov." label="Upload file dengan format .mp4 atau .mov."
onDrop={(files) => setVideoFiles(files)} onDrop={(files) => setVideoFiles(files)}
@ -773,7 +746,9 @@ export default function FormTaskTaNew() {
<div className="space-y-2"> <div className="space-y-2">
<Label>{t("text", { defaultValue: "Text" })}</Label> <Label>{t("text", { defaultValue: "Text" })}</Label>
<FileUploader <FileUploader
accept={{ "pdf/*": [] }} accept={{
"application/pdf": [], // lebih presisi
}}
maxSize={100} maxSize={100}
label="Upload file dengan format .pdf." label="Upload file dengan format .pdf."
onDrop={(files) => setTextFiles(files)} onDrop={(files) => setTextFiles(files)}
@ -794,7 +769,9 @@ export default function FormTaskTaNew() {
downloadFileExtension="webm" downloadFileExtension="webm"
/> />
<FileUploader <FileUploader
accept={{ "mp3/*": [], "wav/*": [] }} accept={{
"audio/*": [], // menerima mp3, wav, webm (untuk hasil rekaman)
}}
maxSize={100} maxSize={100}
label="Upload file dengan format .mp3 atau .wav." label="Upload file dengan format .mp3 atau .wav."
onDrop={(files) => onDrop={(files) =>
@ -811,7 +788,9 @@ export default function FormTaskTaNew() {
key={idx} key={idx}
className="flex flex-row justify-between items-center" className="flex flex-row justify-between items-center"
> >
<p>{t("voice-note", { defaultValue: "Voice Note" })}</p> <p>
{t("voice-note", { defaultValue: "Voice Note" })}
</p>
<Button <Button
type="button" type="button"
onClick={() => handleDeleteAudio(idx)} onClick={() => handleDeleteAudio(idx)}

View File

@ -96,7 +96,7 @@ export async function detailMediaSummary(id: string) {
export async function saveMediaBlastAccount(data: { export async function saveMediaBlastAccount(data: {
accountName: string; accountName: string;
accountType: string; accountType: string;
accountCategory: string; // accountCategory: string;
emailAddress: string; emailAddress: string;
whatsappNumber: string; whatsappNumber: string;
id?: string; id?: string;