feat:update ui kurator,approver
This commit is contained in:
parent
bb0d5e7999
commit
936a7e6e94
|
|
@ -384,7 +384,7 @@ export default function AddExpertForm() {
|
|||
/>
|
||||
|
||||
<div className="mt-4">
|
||||
<FormLabel>Penempatan</FormLabel>
|
||||
<FormLabel>Posisi</FormLabel>
|
||||
{placementRows?.map((row: any) => (
|
||||
<div key={row.index} className="flex items-center gap-2 my-2">
|
||||
<Select
|
||||
|
|
|
|||
|
|
@ -1,7 +1,14 @@
|
|||
import * as React from "react";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
|
||||
import { Eye, MoreVertical, SquarePen, Trash2 } from "lucide-react";
|
||||
import {
|
||||
Eye,
|
||||
MoreVertical,
|
||||
SquarePen,
|
||||
Trash2,
|
||||
Upload,
|
||||
UploadCloud,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
DropdownMenu,
|
||||
|
|
@ -35,11 +42,6 @@ const useTableColumns = () => {
|
|||
<span className="whitespace-normal">{row.getValue("title")}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "categoryName",
|
||||
header: t("category"),
|
||||
cell: ({ row }) => <span>{row.getValue("categoryName")}</span>,
|
||||
},
|
||||
{
|
||||
accessorKey: "createdAt",
|
||||
header: t("upload-date"),
|
||||
|
|
@ -57,33 +59,9 @@ const useTableColumns = () => {
|
|||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "tags",
|
||||
header: t("tag"),
|
||||
cell: ({ row }) => <span className="">{row.getValue("tags")}</span>,
|
||||
},
|
||||
{
|
||||
accessorKey: "statusName",
|
||||
header: "Status",
|
||||
cell: ({ row }) => {
|
||||
const statusColors: Record<string, string> = {
|
||||
diterima: "bg-green-100 text-green-600",
|
||||
"menunggu review": "bg-orange-100 text-orange-600",
|
||||
};
|
||||
|
||||
// Mengambil `statusName` dari data API
|
||||
const status = row.getValue("statusName") as string;
|
||||
const statusName = status?.toLocaleLowerCase(); // Ubah ke huruf kecil
|
||||
|
||||
// Gunakan `statusName` untuk pencocokan
|
||||
const statusStyles =
|
||||
statusColors[statusName] || "bg-gray-100 text-gray-600";
|
||||
|
||||
return (
|
||||
<Badge className={cn("rounded-full px-5", statusStyles)}>
|
||||
{status} {/* Tetap tampilkan nilai asli */}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
accessorKey: "version",
|
||||
header: t("version"),
|
||||
cell: ({ row }) => <span className="">{row.getValue("version")}</span>,
|
||||
},
|
||||
|
||||
{
|
||||
|
|
@ -149,13 +127,13 @@ const useTableColumns = () => {
|
|||
<Link href={`/contributor/blog/detail/${row.original.id}`}>
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
|
||||
<Eye className="w-4 h-4 me-1.5" />
|
||||
View
|
||||
Preview
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<Link href={`/contributor/blog/update/${row.original.id}`}>
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
|
||||
<SquarePen className="w-4 h-4 me-1.5" />
|
||||
Edit
|
||||
<Upload className="w-4 h-4 me-1.5" />
|
||||
Download
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<DropdownMenuItem
|
||||
|
|
|
|||
|
|
@ -0,0 +1,190 @@
|
|||
import * as React from "react";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
|
||||
import { Eye, MoreVertical, SquarePen, Trash2 } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuItem,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { format } from "date-fns";
|
||||
import { Link } from "@/components/navigation";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { deleteCategory } from "@/service/settings/settings";
|
||||
import { deleteTask } from "@/service/task";
|
||||
import { error, loading } from "@/lib/swal";
|
||||
import withReactContent from "sweetalert2-react-content";
|
||||
import Swal from "sweetalert2";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const useTableColumns = () => {
|
||||
const t = useTranslations("Table"); // Panggil di dalam hook
|
||||
const columns: ColumnDef<any>[] = [
|
||||
{
|
||||
accessorKey: "no",
|
||||
header: t("no"),
|
||||
cell: ({ row }) => <span>{row.getValue("no")}</span>,
|
||||
},
|
||||
{
|
||||
accessorKey: "title",
|
||||
header: t("title"),
|
||||
cell: ({ row }) => (
|
||||
<div>
|
||||
<span>{row.getValue("title")}</span>
|
||||
{row.original.isForward && (
|
||||
<Button
|
||||
variant={"outline"}
|
||||
color="primary"
|
||||
size="sm"
|
||||
className="ml-3 rounded-xl"
|
||||
>
|
||||
Forward
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
accessorKey: "createdAt",
|
||||
header: t("upload-date"),
|
||||
cell: ({ row }) => {
|
||||
const createdAt = row.getValue("createdAt") as
|
||||
| string
|
||||
| number
|
||||
| undefined;
|
||||
|
||||
const formattedDate =
|
||||
createdAt && !isNaN(new Date(createdAt).getTime())
|
||||
? format(new Date(createdAt), "dd-MM-yyyy HH:mm:ss")
|
||||
: "-";
|
||||
return <span className="whitespace-nowrap">{formattedDate}</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "status",
|
||||
header: "Status",
|
||||
cell: ({ row }) => {
|
||||
const isActive = row.original.isActive;
|
||||
const isDone = row.original.isDone;
|
||||
|
||||
let statusText = "";
|
||||
if (isDone) {
|
||||
statusText = "Selesai";
|
||||
} else if (isActive) {
|
||||
statusText = "Aktif";
|
||||
} else {
|
||||
statusText = "Nonaktif";
|
||||
}
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
Aktif: "bg-primary/20 text-primary",
|
||||
Selesai: "bg-success/20 text-success",
|
||||
Nonaktif: "bg-gray-200 text-gray-500",
|
||||
};
|
||||
|
||||
const statusStyles = statusColors[statusText] || "default";
|
||||
|
||||
return (
|
||||
<Badge className={cn("rounded-full px-5", statusStyles)}>
|
||||
{statusText}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
accessorKey: "action",
|
||||
header: t("action"),
|
||||
enableHiding: false,
|
||||
cell: ({ row }) => {
|
||||
const router = useRouter();
|
||||
const MySwal = withReactContent(Swal);
|
||||
|
||||
async function deleteProcess(id: any) {
|
||||
loading();
|
||||
const resDelete = await deleteTask(id);
|
||||
|
||||
if (resDelete?.error) {
|
||||
error(resDelete.message);
|
||||
return false;
|
||||
}
|
||||
success();
|
||||
}
|
||||
|
||||
function success() {
|
||||
MySwal.fire({
|
||||
title: "Sukses",
|
||||
icon: "success",
|
||||
confirmButtonColor: "#3085d6",
|
||||
confirmButtonText: "OK",
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const TaskDelete = (id: any) => {
|
||||
MySwal.fire({
|
||||
title: "Hapus Data",
|
||||
text: "",
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
cancelButtonColor: "#3085d6",
|
||||
confirmButtonColor: "#d33",
|
||||
confirmButtonText: "Hapus",
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
deleteProcess(id);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
className="bg-transparent ring-offset-transparent hover:bg-transparent hover:ring-0 hover:ring-transparent"
|
||||
>
|
||||
<span className="sr-only">Open menu</span>
|
||||
<MoreVertical className="h-4 w-4 text-default-800" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="p-0" align="end">
|
||||
<Link href={`/contributor/task/detail/${row.original.id}`}>
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
|
||||
<Eye className="w-4 h-4 me-1.5" />
|
||||
View
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<Link href={`/contributor/task/update/${row.original.id}`}>
|
||||
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
|
||||
<SquarePen className="w-4 h-4 me-1.5" />
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<DropdownMenuItem
|
||||
onClick={() => TaskDelete(row.original.id)}
|
||||
className="p-2 border-b text-destructive bg-destructive/30 focus:bg-destructive focus:text-destructive-foreground rounded-none"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 me-1.5" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return columns;
|
||||
};
|
||||
|
||||
export default useTableColumns;
|
||||
|
|
@ -0,0 +1,405 @@
|
|||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import {
|
||||
ColumnDef,
|
||||
ColumnFiltersState,
|
||||
PaginationState,
|
||||
SortingState,
|
||||
VisibilityState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Eye,
|
||||
MoreVertical,
|
||||
Search,
|
||||
SquarePen,
|
||||
Trash2,
|
||||
TrendingDown,
|
||||
TrendingUp,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
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 TablePagination from "@/components/table/table-pagination";
|
||||
import columns from "./columns";
|
||||
import { listTask } from "@/service/task";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { format } from "date-fns";
|
||||
import { useTranslations } from "next-intl";
|
||||
import useTableColumns from "./columns";
|
||||
|
||||
const TaskTaTable = () => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const t = useTranslations("AnalyticsDashboard");
|
||||
const [dataTable, setDataTable] = React.useState<any[]>([]);
|
||||
const [totalData, setTotalData] = React.useState<number>(1);
|
||||
const [sorting, setSorting] = React.useState<SortingState>([]);
|
||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
||||
[]
|
||||
);
|
||||
const [columnVisibility, setColumnVisibility] =
|
||||
React.useState<VisibilityState>({});
|
||||
const [rowSelection, setRowSelection] = React.useState({});
|
||||
const [showData, setShowData] = React.useState("50");
|
||||
const [pagination, setPagination] = React.useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: Number(showData),
|
||||
});
|
||||
const [statusFilter, setStatusFilter] = React.useState<number[]>([]);
|
||||
const [dateFilter, setDateFilter] = React.useState("");
|
||||
const [endDate, setEndDate] = React.useState("");
|
||||
const [filterByCode, setFilterByCode] = React.useState<string>("");
|
||||
const [page, setPage] = React.useState(1);
|
||||
const [totalPage, setTotalPage] = React.useState(1);
|
||||
const [limit, setLimit] = React.useState(10);
|
||||
const [isSpecificAttention, setIsSpecificAttention] = React.useState(true);
|
||||
const [search, setSearch] = React.useState<string>("");
|
||||
const columns = useTableColumns();
|
||||
const table = useReactTable({
|
||||
data: dataTable,
|
||||
columns,
|
||||
onSortingChange: setSorting,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
onColumnVisibilityChange: setColumnVisibility,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
onPaginationChange: setPagination,
|
||||
state: {
|
||||
sorting,
|
||||
columnFilters,
|
||||
columnVisibility,
|
||||
rowSelection,
|
||||
pagination,
|
||||
},
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
const pageFromUrl = searchParams?.get("page");
|
||||
if (pageFromUrl) {
|
||||
setPage(Number(pageFromUrl));
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
React.useEffect(() => {
|
||||
fetchData();
|
||||
}, [
|
||||
page,
|
||||
showData,
|
||||
isSpecificAttention,
|
||||
search,
|
||||
dateFilter,
|
||||
filterByCode,
|
||||
statusFilter,
|
||||
]);
|
||||
|
||||
async function fetchData() {
|
||||
const formattedStartDate = dateFilter
|
||||
? format(new Date(dateFilter), "yyyy-MM-dd")
|
||||
: "";
|
||||
try {
|
||||
const res = await listTask(
|
||||
page - 1,
|
||||
search,
|
||||
showData,
|
||||
filterByCode,
|
||||
formattedStartDate,
|
||||
isSpecificAttention ? "atensi-khusus" : "tugas-harian",
|
||||
statusFilter
|
||||
);
|
||||
|
||||
const data = res?.data?.data;
|
||||
const contentData = data?.content;
|
||||
|
||||
// let contentDataFilter = res?.data?.data?.content || [];
|
||||
|
||||
// Filter berdasarkan status
|
||||
// contentDataFilter = contentDataFilter.filter((item: any) => {
|
||||
// const isSelesai = statusFilter.includes(1) ? item.isDone : true;
|
||||
// const isAktif = statusFilter.includes(2) ? item.isActive : true;
|
||||
// return isSelesai && isAktif;
|
||||
// });
|
||||
|
||||
contentData.forEach((item: any, index: number) => {
|
||||
item.no = (page - 1) * Number(showData) + index + 1;
|
||||
});
|
||||
|
||||
console.log("contentData : ", contentData);
|
||||
|
||||
setDataTable(contentData);
|
||||
setTotalData(data?.totalElements);
|
||||
setTotalPage(data?.totalPages);
|
||||
} catch (error) {
|
||||
console.error("Error fetching tasks:", error);
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setFilterByCode(e.target.value);
|
||||
setSearch(e.target.value);
|
||||
table.getColumn("judul")?.setFilterValue(e.target.value);
|
||||
};
|
||||
|
||||
function handleStatusCheckboxChange(value: number) {
|
||||
setStatusFilter((prev) =>
|
||||
prev.includes(value)
|
||||
? prev.filter((status) => status !== value)
|
||||
: [...prev, value]
|
||||
);
|
||||
}
|
||||
|
||||
// const handleSearchFilterByCode = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
// const value = e.target.value;
|
||||
// console.log("code :", value);
|
||||
// setFilterByCode(value);
|
||||
// fetchData();
|
||||
// };
|
||||
|
||||
return (
|
||||
<div className="w-full overflow-x-auto">
|
||||
<div className="mx-5 mb-3">
|
||||
<div className="">
|
||||
<div className="row">
|
||||
<div className="flex justify-between mb-6">
|
||||
<label className="inline-flex text-md cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
onChange={() => setIsSpecificAttention(!isSpecificAttention)}
|
||||
hidden
|
||||
/>
|
||||
<span
|
||||
className={` ${
|
||||
isSpecificAttention
|
||||
? "bg-default-900 text-white"
|
||||
: "dark:text-default-700 border-2"
|
||||
}
|
||||
px-[18px] py-1 transition duration-100 rounded`}
|
||||
>
|
||||
{t("special-attention")}
|
||||
</span>
|
||||
<span
|
||||
className={`
|
||||
${
|
||||
!isSpecificAttention
|
||||
? "bg-default-900 text-white"
|
||||
: " dark:text-default-700 border-2"
|
||||
}
|
||||
px-[18px] py-1 transition duration-100 rounded
|
||||
`}
|
||||
>
|
||||
{t("daily-tasks")}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row lg:flex-row justify-between sm:items-center md:items-center lg:items-center px-5">
|
||||
<div className="mb-3 sm:mb-0 lg-mb-0">
|
||||
<InputGroup merged>
|
||||
<InputGroupText className="bg-transparent dark:border-secondary dark:group-focus-within:border-secondary">
|
||||
<Search className=" h-4 w-4 dark:text-white" />
|
||||
</InputGroupText>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search Title dan Code"
|
||||
className="bg-transparent dark:border-secondary dark:placeholder-white/80 dark:focus:border-secondary dark:text-white w-full"
|
||||
value={search}
|
||||
onChange={handleSearch}
|
||||
/>
|
||||
</InputGroup>
|
||||
</div>
|
||||
|
||||
<div className=" flex flex-row items-center gap-3">
|
||||
<div className="flex items-center py-4">
|
||||
<div className="mx-3">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size="md" variant="outline">
|
||||
1 - {showData} Data
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56 text-sm">
|
||||
<DropdownMenuRadioGroup
|
||||
value={showData}
|
||||
onValueChange={setShowData}
|
||||
>
|
||||
<DropdownMenuRadioItem value="10">
|
||||
1 - 10 Data
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="50">
|
||||
1 - 50 Data
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="100">
|
||||
1 - 100 Data
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="250">
|
||||
1 - 250 Data
|
||||
</DropdownMenuRadioItem>
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="ml-auto w-full sm:w-[100px]"
|
||||
size="md"
|
||||
>
|
||||
Filter <ChevronDown />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
className="w-64 h-[200px] overflow-y-auto"
|
||||
>
|
||||
<div className="flex flex-row justify-between my-1 mx-1">
|
||||
<p>Filter</p>
|
||||
</div>
|
||||
<div className="mx-2 my-1">
|
||||
<Label>{t("date")}</Label>
|
||||
<Input
|
||||
type="date"
|
||||
value={dateFilter}
|
||||
onChange={(e) => setDateFilter(e.target.value)}
|
||||
className="max-w-sm"
|
||||
/>
|
||||
</div>
|
||||
{/* <div className="mx-2 my-1">
|
||||
<Label>Code</Label>
|
||||
<Input
|
||||
placeholder="Filter Status..."
|
||||
value={filterByCode}
|
||||
// onChange={handleSearchFilterByCode}
|
||||
className="max-w-sm"
|
||||
/>
|
||||
</div> */}
|
||||
<Label className="ml-2 mt-2">Status</Label>
|
||||
<div className="flex items-center px-4 py-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="status-1"
|
||||
className="mr-2"
|
||||
checked={statusFilter.includes(1)}
|
||||
onChange={() => handleStatusCheckboxChange(1)}
|
||||
/>
|
||||
<label htmlFor="status-1" className="text-sm">
|
||||
{t("done")}
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center px-4 py-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="status-2"
|
||||
className="mr-2"
|
||||
checked={statusFilter.includes(2)}
|
||||
onChange={() => handleStatusCheckboxChange(2)}
|
||||
/>
|
||||
<label htmlFor="status-2" className="text-sm">
|
||||
{t("active")}
|
||||
</label>
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="flex-none">
|
||||
<Input
|
||||
placeholder="Filter Status..."
|
||||
value={
|
||||
(table.getColumn("status")?.getFilterValue() as string) ?? ""
|
||||
}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
table.getColumn("status")?.setFilterValue(event.target.value)
|
||||
}
|
||||
className="max-w-sm "
|
||||
/>
|
||||
</div> */}
|
||||
</div>
|
||||
<Table className="overflow-hidden mt-3">
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id} className="bg-default-200">
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
className="h-[75px]"
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePagination
|
||||
table={table}
|
||||
totalData={totalData}
|
||||
totalPage={totalPage}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskTaTable;
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import SiteBreadcrumb from "@/components/site-breadcrumb";
|
||||
import FormTaskTa from "@/components/form/task-ta/task-ta-form";
|
||||
|
||||
const TaskTaCreatePage = () => {
|
||||
return (
|
||||
<div>
|
||||
<SiteBreadcrumb />
|
||||
<div className="space-y-4">
|
||||
<FormTaskTa />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskTaCreatePage;
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import SiteBreadcrumb from "@/components/site-breadcrumb";
|
||||
import FormTask from "@/components/form/task/task-form";
|
||||
import FormTaskDetail from "@/components/form/task/task-detail-form";
|
||||
|
||||
const TaskTaDetailPage = async () => {
|
||||
return (
|
||||
<div>
|
||||
<SiteBreadcrumb />
|
||||
<div className="space-y-4">
|
||||
<FormTaskDetail />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskTaDetailPage;
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
export const metadata = {
|
||||
title: "Task",
|
||||
};
|
||||
|
||||
const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
"use client";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import TaskTable from "./components/task-ta-table";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { UploadIcon } from "lucide-react";
|
||||
import SiteBreadcrumb from "@/components/site-breadcrumb";
|
||||
import { Link } from "@/components/navigation";
|
||||
import { checkAuthorization, checkLoginSession } from "@/lib/utils";
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import TaskTaTable from "./components/task-ta-table";
|
||||
|
||||
const TaskTaPage = () => {
|
||||
const t = useTranslations("AnalyticsDashboard");
|
||||
useEffect(() => {
|
||||
function initState() {
|
||||
checkAuthorization("admin"); // Specify the page, e.g., "admin" or another value
|
||||
checkLoginSession();
|
||||
}
|
||||
|
||||
initState();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SiteBreadcrumb />
|
||||
<div className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader className="border-b border-solid border-default-200 mb-6">
|
||||
<CardTitle>
|
||||
<div className="flex flex-col sm:flex-row lg:flex-row lg:items-center">
|
||||
<div className="flex-1 text-xl font-medium text-default-900">
|
||||
{t("tabel")} {t("task-ta")}
|
||||
</div>
|
||||
<div className="flex-none">
|
||||
<Link href={"/contributor/task-ta/create"}>
|
||||
<Button color="primary" className="text-white">
|
||||
<UploadIcon size={18} className="mr-2" />
|
||||
{t("create-task")}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<TaskTaTable />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskTaPage;
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import SiteBreadcrumb from "@/components/site-breadcrumb";
|
||||
import FormTask from "@/components/form/task/task-form";
|
||||
import FormTaskDetail from "@/components/form/task/task-detail-form";
|
||||
import FormTaskEdit from "@/components/form/task/task-edit-form";
|
||||
|
||||
const TaskTaDetailPage = async () => {
|
||||
return (
|
||||
<div>
|
||||
<SiteBreadcrumb />
|
||||
<div className="space-y-4">
|
||||
<FormTaskEdit />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskTaDetailPage;
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,929 @@
|
|||
"use client";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import * as z from "zod";
|
||||
import Swal from "sweetalert2";
|
||||
import withReactContent from "sweetalert2-react-content";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import JoditEditor from "jodit-react";
|
||||
import {
|
||||
createTask,
|
||||
getTask,
|
||||
getUserLevelForAssignments,
|
||||
} from "@/service/task";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { ChevronDown, ChevronUp, Trash2 } from "lucide-react";
|
||||
import { AudioRecorder } from "react-audio-voice-recorder";
|
||||
import FileUploader from "@/components/form/shared/file-uploader";
|
||||
import { Upload } from "tus-js-client";
|
||||
import { error } from "@/config/swal";
|
||||
import { getCsrfToken } from "@/service/auth";
|
||||
import { loading } from "@/lib/swal";
|
||||
import { useTranslations } from "next-intl";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
const taskSchema = z.object({
|
||||
title: z.string().min(1, { message: "Judul diperlukan" }),
|
||||
naration: z.string().min(2, {
|
||||
message: "Narasi Penugasan harus lebih dari 2 karakter.",
|
||||
}),
|
||||
// url: z.string().min(1, { message: "Judul diperlukan" }),
|
||||
});
|
||||
|
||||
interface FileWithPreview extends File {
|
||||
preview: string;
|
||||
}
|
||||
|
||||
export type taskDetail = {
|
||||
id: number;
|
||||
title: string;
|
||||
fileTypeOutput: string;
|
||||
assignedToTopLevel: string;
|
||||
assignedToLevel: string;
|
||||
assignmentType: {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
assignmentMainType: {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
attachmentUrl: string;
|
||||
taskType: string;
|
||||
broadcastType: string;
|
||||
narration: string;
|
||||
is_active: string;
|
||||
};
|
||||
|
||||
const CustomEditor = dynamic(
|
||||
() => {
|
||||
return import("@/components/editor/custom-editor");
|
||||
},
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
export default function FormTaskTa() {
|
||||
const MySwal = withReactContent(Swal);
|
||||
const router = useRouter();
|
||||
const editor = useRef(null);
|
||||
type TaskSchema = z.infer<typeof taskSchema>;
|
||||
const { id } = useParams() as { id: string };
|
||||
console.log(id);
|
||||
|
||||
// State for various form fields
|
||||
const [expertise, setExpertiseOutput] = useState({
|
||||
semua: false,
|
||||
komunikasi: false,
|
||||
hukum: false,
|
||||
bahasa: false,
|
||||
ekonomi: false,
|
||||
politik: false,
|
||||
sosiologi: false,
|
||||
ilmuadministrasipemerintah: false,
|
||||
ti: false,
|
||||
});
|
||||
const [expert, setExpertOutput] = useState({
|
||||
semua: false,
|
||||
});
|
||||
|
||||
// const [assignmentType, setAssignmentType] = useState("mediahub");
|
||||
// const [assignmentCategory, setAssignmentCategory] = useState("publication");
|
||||
const [mainType, setMainType] = useState<string>("1");
|
||||
const [taskType, setTaskType] = useState<string>("atensi-khusus");
|
||||
const [broadcastType, setBroadcastType] = useState<string>("");
|
||||
const [type, setType] = useState<string>("1");
|
||||
const [selectedTarget, setSelectedTarget] = useState("3,4");
|
||||
const [detail, setDetail] = useState<taskDetail>();
|
||||
const [refresh] = useState(false);
|
||||
const [listDest, setListDest] = useState([]);
|
||||
const [checkedLevels, setCheckedLevels] = useState(new Set());
|
||||
const [expandedPolda, setExpandedPolda] = useState([{}]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [audioFile, setAudioFile] = useState<File | null>(null);
|
||||
const [isRecording, setIsRecording] = useState(false);
|
||||
const [timer, setTimer] = useState<number>(120);
|
||||
|
||||
const t = useTranslations("Form");
|
||||
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 [platformTypeVisible, setPlatformTypeVisible] = useState(false);
|
||||
const [unitSelection, setUnitSelection] = useState({
|
||||
semua: false,
|
||||
mabes: false,
|
||||
polda: false,
|
||||
polres: false,
|
||||
satker: false,
|
||||
});
|
||||
const [links, setLinks] = useState<string[]>([""]);
|
||||
|
||||
const {
|
||||
register,
|
||||
control,
|
||||
setValue,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm<TaskSchema>({
|
||||
resolver: zodResolver(taskSchema),
|
||||
mode: "all",
|
||||
});
|
||||
|
||||
// const handleRadioChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
// const selectedValue = Number(event.target.value);
|
||||
// setMainType(selectedValue);
|
||||
|
||||
// setPlatformTypeVisible(selectedValue === 2);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchPoldaPolres() {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await getUserLevelForAssignments();
|
||||
setListDest(response?.data?.data.list);
|
||||
console.log("polda", response?.data?.data?.list);
|
||||
const initialExpandedState = response?.data?.data.list.reduce(
|
||||
(acc: any, polda: any) => {
|
||||
acc[polda.id] = false;
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
setExpandedPolda(initialExpandedState);
|
||||
console.log("polres", initialExpandedState);
|
||||
} catch (error) {
|
||||
console.error("Error fetching Polda/Polres data:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
fetchPoldaPolres();
|
||||
}, []);
|
||||
|
||||
// };
|
||||
const handleCheckboxChange = (levelId: number) => {
|
||||
setCheckedLevels((prev) => {
|
||||
const updatedLevels = new Set(prev);
|
||||
if (updatedLevels.has(levelId)) {
|
||||
updatedLevels.delete(levelId);
|
||||
} else {
|
||||
updatedLevels.add(levelId);
|
||||
}
|
||||
return updatedLevels;
|
||||
});
|
||||
};
|
||||
|
||||
const handlePoldaPolresChange = () => {
|
||||
return Array.from(checkedLevels).join(","); // Mengonversi Set ke string
|
||||
};
|
||||
|
||||
const handleUnitChange = (
|
||||
key: keyof typeof unitSelection,
|
||||
value: boolean
|
||||
) => {
|
||||
if (key === "semua") {
|
||||
const newState = {
|
||||
semua: value,
|
||||
mabes: value,
|
||||
polda: value,
|
||||
polres: value,
|
||||
satker: value,
|
||||
};
|
||||
setUnitSelection(newState);
|
||||
} else {
|
||||
const updatedSelection = {
|
||||
...unitSelection,
|
||||
[key]: value,
|
||||
};
|
||||
|
||||
const allChecked = ["mabes", "polda", "polres", "satker"].every(
|
||||
(k) => updatedSelection[k as keyof typeof unitSelection]
|
||||
);
|
||||
|
||||
updatedSelection.semua = allChecked;
|
||||
|
||||
setUnitSelection(updatedSelection);
|
||||
}
|
||||
};
|
||||
|
||||
const handleExpertiseOutputChange = (
|
||||
key: keyof typeof expertise,
|
||||
value: boolean
|
||||
) => {
|
||||
if (key === "semua") {
|
||||
const newState = {
|
||||
semua: value,
|
||||
komunikasi: value,
|
||||
hukum: value,
|
||||
bahasa: value,
|
||||
ekonomi: value,
|
||||
politik: value,
|
||||
sosiologi: value,
|
||||
ilmuadministrasipemerintah: value,
|
||||
ti: value,
|
||||
};
|
||||
setExpertiseOutput(newState);
|
||||
} else {
|
||||
const updated = {
|
||||
...expertise,
|
||||
[key]: value,
|
||||
};
|
||||
|
||||
const allChecked = ["video", "audio", "image", "text"].every(
|
||||
(k) => updated[k as keyof typeof expertise]
|
||||
);
|
||||
|
||||
updated.semua = allChecked;
|
||||
setExpertiseOutput(updated);
|
||||
}
|
||||
};
|
||||
|
||||
const handleExpertOutputChange = (
|
||||
key: keyof typeof expert,
|
||||
value: boolean
|
||||
) => {
|
||||
if (key === "semua") {
|
||||
const newState = {
|
||||
semua: value,
|
||||
};
|
||||
setExpertOutput(newState);
|
||||
} else {
|
||||
const updated = {
|
||||
...expert,
|
||||
[key]: value,
|
||||
};
|
||||
|
||||
const allChecked = ["video", "audio", "image", "text"].every(
|
||||
(k) => updated[k as keyof typeof expert]
|
||||
);
|
||||
|
||||
updated.semua = allChecked;
|
||||
setExpertOutput(updated);
|
||||
}
|
||||
};
|
||||
|
||||
const save = async (data: TaskSchema) => {
|
||||
const fileTypeMapping = {
|
||||
all: "1",
|
||||
video: "2",
|
||||
audio: "4",
|
||||
image: "3",
|
||||
text: "5",
|
||||
};
|
||||
|
||||
const unitMapping = {
|
||||
allUnit: "0",
|
||||
mabes: "1",
|
||||
polda: "2",
|
||||
polres: "3",
|
||||
satker: "4",
|
||||
};
|
||||
const assignmentPurposeString = Object.keys(unitSelection)
|
||||
.filter((key) => unitSelection[key as keyof typeof unitSelection])
|
||||
.map((key) => unitMapping[key as keyof typeof unitMapping])
|
||||
.join(",");
|
||||
|
||||
const selectedOutputs = Object.keys(expertise)
|
||||
.filter((key) => expertise[key as keyof typeof expertise])
|
||||
.map((key) => fileTypeMapping[key as keyof typeof fileTypeMapping])
|
||||
.join(",");
|
||||
|
||||
const requestData: {
|
||||
id?: number;
|
||||
title: string;
|
||||
assignedToLevel: any;
|
||||
assignmentPurpose: any;
|
||||
assignmentTypeId: string;
|
||||
fileTypeOutput: string;
|
||||
narration: string;
|
||||
platformType: string | null;
|
||||
assignmentMainTypeId: any;
|
||||
taskType: string;
|
||||
assignedToRole: string;
|
||||
broadcastType: string;
|
||||
attachmentUrl: string[];
|
||||
} = {
|
||||
...data,
|
||||
// assignmentType,
|
||||
// assignmentCategory,
|
||||
assignedToLevel: handlePoldaPolresChange(),
|
||||
assignmentPurpose: assignmentPurposeString,
|
||||
assignedToRole: selectedTarget,
|
||||
taskType: taskType,
|
||||
broadcastType: broadcastType,
|
||||
assignmentMainTypeId: mainType,
|
||||
assignmentTypeId: type,
|
||||
fileTypeOutput: selectedOutputs,
|
||||
narration: data.naration,
|
||||
platformType: "",
|
||||
title: data.title,
|
||||
attachmentUrl: links,
|
||||
};
|
||||
|
||||
const response = await createTask(requestData);
|
||||
|
||||
console.log("Form Data Submitted:", requestData);
|
||||
console.log("response", response);
|
||||
|
||||
const id = response?.data?.data.id;
|
||||
loading();
|
||||
if (imageFiles?.length == 0) {
|
||||
setIsImageUploadFinish(true);
|
||||
}
|
||||
imageFiles?.map(async (item: any, index: number) => {
|
||||
await uploadResumableFile(index, String(id), item, "1", "0");
|
||||
});
|
||||
|
||||
if (videoFiles?.length == 0) {
|
||||
setIsVideoUploadFinish(true);
|
||||
}
|
||||
videoFiles?.map(async (item: any, index: number) => {
|
||||
await uploadResumableFile(index, String(id), item, "2", "0");
|
||||
});
|
||||
|
||||
if (textFiles?.length == 0) {
|
||||
setIsTextUploadFinish(true);
|
||||
}
|
||||
textFiles?.map(async (item: any, index: number) => {
|
||||
await uploadResumableFile(index, String(id), item, "3", "0");
|
||||
});
|
||||
|
||||
if (audioFiles?.length == 0) {
|
||||
setIsAudioUploadFinish(true);
|
||||
}
|
||||
audioFiles.map(async (item: FileWithPreview, index: number) => {
|
||||
await uploadResumableFile(
|
||||
index,
|
||||
String(id),
|
||||
item, // Use .file to access the actual File object
|
||||
"4",
|
||||
"0" // Optional: Replace with actual duration if available
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const onSubmit = (data: TaskSchema) => {
|
||||
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 toggleExpand = (poldaId: any) => {
|
||||
setExpandedPolda((prev: any) => ({
|
||||
...prev,
|
||||
[poldaId]: !prev[poldaId],
|
||||
}));
|
||||
};
|
||||
|
||||
const onRecordingStart = () => {
|
||||
setIsRecording(true);
|
||||
|
||||
const countdown = setInterval(() => {
|
||||
setTimer((prevTimer) => {
|
||||
if (prevTimer <= 1) {
|
||||
clearInterval(countdown);
|
||||
return 0;
|
||||
}
|
||||
return prevTimer - 1;
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
setTimeout(() => {
|
||||
if (isRecording) {
|
||||
handleStopRecording();
|
||||
}
|
||||
}, 120000);
|
||||
};
|
||||
|
||||
const handleStopRecording = () => {
|
||||
setIsRecording(false);
|
||||
setTimer(120); // Reset the timer to 2 minutes for the next recording
|
||||
};
|
||||
|
||||
const addAudioElement = (blob: Blob) => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const audio = document.createElement("audio");
|
||||
audio.src = url;
|
||||
audio.controls = true;
|
||||
document.body.appendChild(audio);
|
||||
|
||||
// Convert Blob to File and add preview
|
||||
const fileWithPreview: FileWithPreview = Object.assign(
|
||||
new File([blob], "voiceNote.webm", { type: "audio/webm" }),
|
||||
{ preview: url }
|
||||
);
|
||||
|
||||
// Add to state
|
||||
setAudioFile(fileWithPreview);
|
||||
setAudioFiles((prev) => [...prev, fileWithPreview]);
|
||||
};
|
||||
|
||||
const handleDeleteAudio = (index: number) => {
|
||||
setAudioFiles((prev) => prev.filter((_, idx) => idx !== index));
|
||||
};
|
||||
|
||||
async function uploadResumableFile(
|
||||
idx: number,
|
||||
id: string,
|
||||
file: any,
|
||||
fileTypeId: string,
|
||||
duration: string
|
||||
) {
|
||||
console.log(idx, id, file, fileTypeId, duration);
|
||||
|
||||
const resCsrf = await getCsrfToken();
|
||||
const csrfToken = resCsrf?.data?.token;
|
||||
console.log("CSRF TOKEN : ", csrfToken);
|
||||
const headers = {
|
||||
"X-XSRF-TOKEN": csrfToken,
|
||||
};
|
||||
|
||||
const upload = new Upload(file, {
|
||||
endpoint: `${process.env.NEXT_PUBLIC_API}/assignment/file/upload`,
|
||||
headers: headers,
|
||||
retryDelays: [0, 3000, 6000, 12_000, 24_000],
|
||||
chunkSize: 20_000,
|
||||
metadata: {
|
||||
assignmentId: id,
|
||||
filename: file.name,
|
||||
contentType: file.type,
|
||||
fileTypeId: fileTypeId,
|
||||
duration,
|
||||
},
|
||||
onBeforeRequest: function (req) {
|
||||
var xhr = req.getUnderlyingObject();
|
||||
xhr.withCredentials = true;
|
||||
},
|
||||
onError: async (e: any) => {
|
||||
console.log("Error upload :", e);
|
||||
error(e);
|
||||
},
|
||||
onChunkComplete: (
|
||||
chunkSize: any,
|
||||
bytesAccepted: any,
|
||||
bytesTotal: any
|
||||
) => {
|
||||
// const uploadPersen = Math.floor((bytesAccepted / bytesTotal) * 100);
|
||||
// progressInfo[idx].percentage = uploadPersen;
|
||||
// counterUpdateProgress++;
|
||||
// console.log(counterUpdateProgress);
|
||||
// setProgressList(progressInfo);
|
||||
// setCounterProgress(counterUpdateProgress);
|
||||
},
|
||||
onSuccess: async () => {
|
||||
// uploadPersen = 100;
|
||||
// progressInfo[idx].percentage = 100;
|
||||
// counterUpdateProgress++;
|
||||
// setCounterProgress(counterUpdateProgress);
|
||||
successTodo();
|
||||
if (fileTypeId == "1") {
|
||||
setIsImageUploadFinish(true);
|
||||
} else if (fileTypeId == "2") {
|
||||
setIsVideoUploadFinish(true);
|
||||
}
|
||||
if (fileTypeId == "3") {
|
||||
setIsTextUploadFinish(true);
|
||||
}
|
||||
if (fileTypeId == "4") {
|
||||
setIsAudioUploadFinish(true);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
upload.start();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
successTodo();
|
||||
}, [
|
||||
isImageUploadFinish,
|
||||
isVideoUploadFinish,
|
||||
isAudioUploadFinish,
|
||||
isTextUploadFinish,
|
||||
]);
|
||||
|
||||
function successTodo() {
|
||||
if (
|
||||
isImageUploadFinish &&
|
||||
isVideoUploadFinish &&
|
||||
isAudioUploadFinish &&
|
||||
isTextUploadFinish
|
||||
) {
|
||||
successSubmit("/in/contributor/task");
|
||||
}
|
||||
}
|
||||
|
||||
const successSubmit = (redirect: string) => {
|
||||
MySwal.fire({
|
||||
title: "Sukses",
|
||||
text: "Data berhasil disimpan.",
|
||||
icon: "success",
|
||||
confirmButtonColor: "#3085d6",
|
||||
confirmButtonText: "OK",
|
||||
}).then(() => {
|
||||
router.push(redirect);
|
||||
});
|
||||
};
|
||||
|
||||
const handleLinkChange = (index: number, value: string) => {
|
||||
const updatedLinks = [...links];
|
||||
updatedLinks[index] = value;
|
||||
setLinks(updatedLinks);
|
||||
};
|
||||
|
||||
const handleAddRow = () => {
|
||||
setLinks([...links, ""]);
|
||||
};
|
||||
|
||||
// Remove a specific link row
|
||||
const handleRemoveRow = (index: number) => {
|
||||
const updatedLinks = links.filter((_: any, i: any) => i !== index);
|
||||
setLinks(updatedLinks);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div className="px-6 py-6">
|
||||
<p className="text-lg font-semibold mb-3">{t("form-task")}</p>
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="gap-5 mb-5">
|
||||
{/* Input Title */}
|
||||
<div className="space-y-2">
|
||||
<Label>{t("title")}</Label>
|
||||
<Controller
|
||||
control={control}
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
size="md"
|
||||
type="text"
|
||||
value={detail?.title}
|
||||
onChange={field.onChange}
|
||||
placeholder="Enter Title"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.title?.message && (
|
||||
<p className="text-red-400 text-sm">{errors.title.message}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row lg:flex-row sm:items-center lg:items-center">
|
||||
<div className="mt-5 space-y-2">
|
||||
<Label>{t("assignment-selection")}</Label>
|
||||
<Select onValueChange={setSelectedTarget}>
|
||||
<SelectTrigger size="md">
|
||||
<SelectValue placeholder="Choose" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="3,4">Semua Pengguna</SelectItem>
|
||||
<SelectItem value="4">Kontributor</SelectItem>
|
||||
<SelectItem value="3">Approver</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-3 mt-5 lg:pt-7 lg:ml-3 ">
|
||||
{Object.keys(unitSelection).map((key) => (
|
||||
<div className="flex items-center gap-2" key={key}>
|
||||
<Checkbox
|
||||
id={key}
|
||||
checked={unitSelection[key as keyof typeof unitSelection]}
|
||||
onCheckedChange={(value) =>
|
||||
handleUnitChange(
|
||||
key as keyof typeof unitSelection,
|
||||
value as boolean
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Label htmlFor={key}>
|
||||
{key.charAt(0).toUpperCase() + key.slice(1)}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-6 lg:pt-6 lg:pl-3">
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="soft" size="sm" color="primary">
|
||||
[{t("custom")}]
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[425px] md:max-w-[500px] lg:max-w-[1500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Daftar Wilayah Polda dan Polres</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="grid grid-cols-2 gap-2 max-h-[400px] overflow-y-auto">
|
||||
{listDest.map((polda: any) => (
|
||||
<div key={polda.id} className="border p-2">
|
||||
<Label className="flex items-center">
|
||||
<Checkbox
|
||||
checked={checkedLevels.has(polda.id)}
|
||||
onCheckedChange={() =>
|
||||
handleCheckboxChange(polda.id)
|
||||
}
|
||||
className="mr-3"
|
||||
/>
|
||||
{polda.name}
|
||||
<button
|
||||
onClick={() => toggleExpand(polda.id)}
|
||||
className="ml-2 focus:outline-none"
|
||||
>
|
||||
{expandedPolda[polda.id] ? (
|
||||
<ChevronUp size={16} />
|
||||
) : (
|
||||
<ChevronDown size={16} />
|
||||
)}
|
||||
</button>
|
||||
</Label>
|
||||
{expandedPolda[polda.id] && (
|
||||
<div className="ml-6 mt-2">
|
||||
<Label className="block">
|
||||
<Checkbox
|
||||
checked={polda?.subDestination?.every(
|
||||
(polres: any) =>
|
||||
checkedLevels.has(polres.id)
|
||||
)}
|
||||
onCheckedChange={(isChecked) => {
|
||||
const updatedLevels = new Set(
|
||||
checkedLevels
|
||||
);
|
||||
polda?.subDestination?.forEach(
|
||||
(polres: any) => {
|
||||
if (isChecked) {
|
||||
updatedLevels.add(polres.id);
|
||||
} else {
|
||||
updatedLevels.delete(polres.id);
|
||||
}
|
||||
}
|
||||
);
|
||||
setCheckedLevels(updatedLevels);
|
||||
}}
|
||||
className="mr-2"
|
||||
/>
|
||||
Pilih Semua Polres
|
||||
</Label>
|
||||
{polda?.subDestination?.map((polres: any) => (
|
||||
<Label key={polres.id} className="block mt-1">
|
||||
<Checkbox
|
||||
checked={checkedLevels.has(polres.id)}
|
||||
onCheckedChange={() =>
|
||||
handleCheckboxChange(polres.id)
|
||||
}
|
||||
className="mr-2"
|
||||
/>
|
||||
{polres.name}
|
||||
</Label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 space-y-2">
|
||||
<Label>{t("areas-expertise")}</Label>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
{Object.keys(expertise).map((key) => (
|
||||
<div className="flex items-center gap-2" key={key}>
|
||||
<Checkbox
|
||||
id={key}
|
||||
checked={expertise[key as keyof typeof expertise]}
|
||||
onCheckedChange={(value) =>
|
||||
handleExpertiseOutputChange(
|
||||
key as keyof typeof expertise,
|
||||
value as boolean
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Label htmlFor={key}>
|
||||
{key.charAt(0).toUpperCase() + key.slice(1)}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 space-y-2">
|
||||
<Label>{t("choose-expert")}</Label>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
{Object.keys(expert).map((key) => (
|
||||
<div className="flex items-center gap-2" key={key}>
|
||||
<Checkbox
|
||||
id={key}
|
||||
checked={expert[key as keyof typeof expert]}
|
||||
onCheckedChange={(value) =>
|
||||
handleExpertOutputChange(
|
||||
key as keyof typeof expert,
|
||||
value as boolean
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Label htmlFor={key}>
|
||||
{key.charAt(0).toUpperCase() + key.slice(1)}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="mt-5">
|
||||
<Label>Broadcast </Label>
|
||||
<RadioGroup
|
||||
value={broadcastType} // Nilai terpilih diambil dari state broadcastType
|
||||
onValueChange={(value) => setBroadcastType(value)} // Mengatur nilai saat radio berubah
|
||||
className="flex flex-wrap gap-3"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="all" id="all" />
|
||||
<Label htmlFor="all">Semua</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="email" id="email" />
|
||||
<Label htmlFor="email">Email Blast</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="whatsapp" id="whatsapp" />
|
||||
<Label htmlFor="whatsapp">WhatsApp Blast</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div> */}
|
||||
<div className="mt-5 space-y-2">
|
||||
<Label>{t("description")}</Label>
|
||||
<Controller
|
||||
control={control}
|
||||
name="naration"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<CustomEditor onChange={onChange} initialData={value} />
|
||||
)}
|
||||
/>
|
||||
{errors.naration?.message && (
|
||||
<p className="text-red-400 text-sm">
|
||||
{errors.naration.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-2.5 mt-5">
|
||||
<Label htmlFor="attachments">{t("attachment")}</Label>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label>{t("audio-visual")}</Label>
|
||||
<FileUploader
|
||||
accept={{
|
||||
"mp4/*": [],
|
||||
"mov/*": [],
|
||||
}}
|
||||
maxSize={100}
|
||||
label="Upload file dengan format .mp4 atau .mov."
|
||||
onDrop={(files) => setVideoFiles(files)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>{t("image")}</Label>
|
||||
<FileUploader
|
||||
accept={{
|
||||
"image/*": [],
|
||||
}}
|
||||
maxSize={100}
|
||||
label="Upload file dengan format .png, .jpg, atau .jpeg."
|
||||
onDrop={(files) => setImageFiles(files)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>{t("text")}</Label>
|
||||
<FileUploader
|
||||
accept={{
|
||||
"pdf/*": [],
|
||||
}}
|
||||
maxSize={100}
|
||||
label="Upload file dengan format .pdf."
|
||||
onDrop={(files) => setTextFiles(files)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>{t("audio")}</Label>
|
||||
<AudioRecorder
|
||||
onRecordingComplete={addAudioElement}
|
||||
audioTrackConstraints={{
|
||||
noiseSuppression: true,
|
||||
echoCancellation: true,
|
||||
}}
|
||||
downloadOnSavePress={true}
|
||||
downloadFileExtension="webm"
|
||||
/>
|
||||
<FileUploader
|
||||
accept={{
|
||||
"mp3/*": [],
|
||||
"wav/*": [],
|
||||
}}
|
||||
maxSize={100}
|
||||
label="Upload file dengan format .mp3 atau .wav."
|
||||
onDrop={(files) =>
|
||||
setAudioFiles((prev) => [...prev, ...files])
|
||||
}
|
||||
className="mt-2"
|
||||
/>
|
||||
</div>
|
||||
{audioFiles?.map((audio: any, idx: any) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="flex flex-row justify-between items-center"
|
||||
>
|
||||
<p>{t("voice-note")}</p>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => handleDeleteAudio(idx)}
|
||||
size="sm"
|
||||
color="destructive"
|
||||
>
|
||||
X
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
{isRecording && <p>Recording... {timer} seconds remaining</p>}{" "}
|
||||
{/* Display remaining time */}
|
||||
<div className="mt-4 space-y-2">
|
||||
<Label className="">{t("news-links")}</Label>
|
||||
{links.map((link, index) => (
|
||||
<div key={index} className="flex items-center gap-2 mt-2">
|
||||
<Input
|
||||
type="url"
|
||||
className="border rounded p-2 w-full"
|
||||
placeholder={`Masukkan link berita ${index + 1}`}
|
||||
value={link}
|
||||
onChange={(e) =>
|
||||
handleLinkChange(index, e.target.value)
|
||||
}
|
||||
/>
|
||||
{links.length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
className="bg-red-500 text-white px-3 py-1 rounded"
|
||||
onClick={() => handleRemoveRow(index)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
type="button"
|
||||
className="mt-2 bg-blue-500 text-white px-4 py-2 rounded"
|
||||
onClick={handleAddRow}
|
||||
size="sm"
|
||||
>
|
||||
{t("add-links")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<div className="mt-4">
|
||||
<Button type="submit" color="primary">
|
||||
{t("submit")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
16
lib/menus.ts
16
lib/menus.ts
|
|
@ -2900,7 +2900,21 @@ export function getMenuList(pathname: string, t: any): Group[] {
|
|||
},
|
||||
{
|
||||
groupLabel: "",
|
||||
id: "curatedcontent",
|
||||
id: "tatask ",
|
||||
menus: [
|
||||
{
|
||||
id: "tatask",
|
||||
href: "/contributor/task-ta",
|
||||
label: t("task-ta"),
|
||||
active: pathname.includes("/task-ta"),
|
||||
icon: "fluent:clipboard-task-add-24-regular",
|
||||
submenus: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
groupLabel: "",
|
||||
id: "curatedcontent ",
|
||||
menus: [
|
||||
{
|
||||
id: "curatedcontent",
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@
|
|||
"Hasil_unggah_ditolak_hari_ini": "Upload result rejected today",
|
||||
"task-routine": "Task Routine",
|
||||
"task": "Task",
|
||||
"task-ta": "Task TA",
|
||||
"schedule": "Schedule",
|
||||
"indeks": "Blog",
|
||||
"report": "Report",
|
||||
|
|
@ -160,6 +161,7 @@
|
|||
"text": "Text",
|
||||
"agenda-setting": "Agenda Setting",
|
||||
"task": "Task",
|
||||
"task-ta": "Task TA",
|
||||
"planning": "Planning",
|
||||
"schedule": "Schedule",
|
||||
"press-conference": "Press Conference",
|
||||
|
|
@ -607,6 +609,14 @@
|
|||
"acc": "Diterima",
|
||||
"wait-review": "Menunggu Review"
|
||||
},
|
||||
"Tast-ta": {
|
||||
"table": "Table",
|
||||
"task-ta": "Task Ta",
|
||||
"generate-report": "Generate Laporan",
|
||||
"category": "Kategori",
|
||||
"acc": "Diterima",
|
||||
"wait-review": "Menunggu Review"
|
||||
},
|
||||
"Communication": {
|
||||
"internal-questions": "Internal Question",
|
||||
"escalation": "Escalation",
|
||||
|
|
@ -649,6 +659,7 @@
|
|||
"duration": "Duration",
|
||||
"target-output": "Target Output",
|
||||
"target-participant": "Target Participant",
|
||||
"version": "Version",
|
||||
"action": "Actions"
|
||||
},
|
||||
"Form": {
|
||||
|
|
@ -663,7 +674,9 @@
|
|||
"category": "Category",
|
||||
"tag": "Tag",
|
||||
"type-content": "Content Type",
|
||||
"choose-expert": "Choose Expert",
|
||||
"type-task": "Task Type",
|
||||
"areas-expertise": "Areas of Expertise",
|
||||
"category-task": "Category Task",
|
||||
"code": "Code",
|
||||
"start-date": "Start Date",
|
||||
|
|
@ -727,7 +740,7 @@
|
|||
"assignment-type": "Assignment Type",
|
||||
"description-task": "Description Task",
|
||||
"form-task": "Form Task",
|
||||
"assignment-selection": "Assignment Selection",
|
||||
"assignment-selection": "Assignment Recipient",
|
||||
"custom": "Costum",
|
||||
"assigment-type": "Assigment Type",
|
||||
"type-of-task": "Type of Task",
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
"Hasil_unggah_ditolak_hari_ini": "Hasil unggah ditolak hari ini",
|
||||
"task-routine": "Tugas Rutin",
|
||||
"task": "Penugasan",
|
||||
"task-ta": "Penugasan TA",
|
||||
"schedule": "Jadwal",
|
||||
"indeks": "Indeks",
|
||||
"report": "Laporan",
|
||||
|
|
@ -40,7 +41,7 @@
|
|||
"create-audio": "Unggah Audio",
|
||||
"create-task": "Buat Penugasan",
|
||||
"special-attention": "Atensi Khusus",
|
||||
"daily-tasks": "Tugas Khusus",
|
||||
"daily-tasks": "Tugas Harian",
|
||||
"date": "Tanggal",
|
||||
"active": "Aktif",
|
||||
"done": "Selesai",
|
||||
|
|
@ -159,6 +160,7 @@
|
|||
"video": "Audio Visual",
|
||||
"audio": "Audio",
|
||||
"text": "Teks",
|
||||
"task-ta": "Penugasan TA",
|
||||
"agenda-setting": "Agenda Setting",
|
||||
"task": "Penugasan",
|
||||
"planning": "Perencanaan",
|
||||
|
|
@ -608,6 +610,14 @@
|
|||
"acc": "Diterima",
|
||||
"wait-review": "Menunggu Review"
|
||||
},
|
||||
"Tast-ta": {
|
||||
"table": "Tabel",
|
||||
"task-ta": "Penugasan TA",
|
||||
"generate-report": "Generate Laporan",
|
||||
"category": "Kategori",
|
||||
"acc": "Diterima",
|
||||
"wait-review": "Menunggu Review"
|
||||
},
|
||||
"Communication": {
|
||||
"internal-questions": "Pertanyaan Internal",
|
||||
"escalation": "Eskalasi",
|
||||
|
|
@ -650,6 +660,7 @@
|
|||
"duration": "Durasi",
|
||||
"target-output": "Target Output",
|
||||
"target-participant": "Target Peserta",
|
||||
"version": "Versi",
|
||||
"action": "Aksi"
|
||||
},
|
||||
"Form": {
|
||||
|
|
@ -664,8 +675,10 @@
|
|||
"category": "Kategori",
|
||||
"tag": "Tag",
|
||||
"type-content": "Tipe Konten",
|
||||
"type-task": "Tipen Penugasan",
|
||||
"type-task": "Tipe Penugasan",
|
||||
"category-task": "Kategori Penugasan",
|
||||
"areas-expertise": "Bidang Keahlian",
|
||||
"choose-expert": "Pilih Tenaga Ahli",
|
||||
"code": "Kode",
|
||||
"start-date": "Tanggal Mulai",
|
||||
"end-date": "Tanggal Selesai",
|
||||
|
|
@ -727,7 +740,7 @@
|
|||
"assignment-type": "Jenis Penugasan",
|
||||
"description-task": "Narasi Penugasan",
|
||||
"form-task": "Form Penugasan",
|
||||
"assignment-selection": "Pemilihan Tugas",
|
||||
"assignment-selection": "Penerima Tugas",
|
||||
"custom": "Kostum",
|
||||
"assigment-type": "Jenis Tugas",
|
||||
"type-of-task": "Jenis Penugasan",
|
||||
|
|
|
|||
Loading…
Reference in New Issue