diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 3ccc229..28c8827 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -257,7 +257,8 @@ model DivisionProject { Division Division @relation(fields: [idDivision], references: [id]) idDivision String title String - desc String @db.Text + desc String? @db.Text + reason String? @db.Text status Int @default(0) // 0 = pending, 1 = ongoing, 2 = done, 3 = cancelled isActive Boolean @default(true) createdAt DateTime @default(now()) diff --git a/src/app/(application)/division/[id]/(fitur-division)/task/[detail]/add-member/page.tsx b/src/app/(application)/division/[id]/(fitur-division)/task/[detail]/add-member/page.tsx new file mode 100644 index 0000000..9f9852e --- /dev/null +++ b/src/app/(application)/division/[id]/(fitur-division)/task/[detail]/add-member/page.tsx @@ -0,0 +1,9 @@ +import { AddMemberDetailTask } from "@/module/task" + +function Page() { + return ( + + ) +} + +export default Page \ No newline at end of file diff --git a/src/app/(application)/division/[id]/(fitur-division)/task/[detail]/add-task/page.tsx b/src/app/(application)/division/[id]/(fitur-division)/task/[detail]/add-task/page.tsx new file mode 100644 index 0000000..64459c4 --- /dev/null +++ b/src/app/(application)/division/[id]/(fitur-division)/task/[detail]/add-task/page.tsx @@ -0,0 +1,9 @@ +import { AddDetailTask } from "@/module/task" + +function Page() { + return ( + + ) +} + +export default Page \ No newline at end of file diff --git a/src/app/(application)/division/[id]/(fitur-division)/task/[detail]/cancel/page.tsx b/src/app/(application)/division/[id]/(fitur-division)/task/[detail]/cancel/page.tsx new file mode 100644 index 0000000..e63ee85 --- /dev/null +++ b/src/app/(application)/division/[id]/(fitur-division)/task/[detail]/cancel/page.tsx @@ -0,0 +1,9 @@ +import { CancelTask } from "@/module/task" + +function Page() { + return ( + + ) +} + +export default Page \ No newline at end of file diff --git a/src/app/(application)/division/[id]/(fitur-division)/task/[detail]/edit/page.tsx b/src/app/(application)/division/[id]/(fitur-division)/task/[detail]/edit/page.tsx new file mode 100644 index 0000000..290ecb6 --- /dev/null +++ b/src/app/(application)/division/[id]/(fitur-division)/task/[detail]/edit/page.tsx @@ -0,0 +1,9 @@ +import { EditTask } from "@/module/task" + +function Page() { + return ( + + ) +} + +export default Page \ No newline at end of file diff --git a/src/app/api/task/[id]/member/route.ts b/src/app/api/task/[id]/member/route.ts new file mode 100644 index 0000000..02e96e2 --- /dev/null +++ b/src/app/api/task/[id]/member/route.ts @@ -0,0 +1,61 @@ +import { prisma } from "@/module/_global"; +import { funGetUserByCookies } from "@/module/auth"; +import _ from "lodash"; +import moment from "moment"; +import { NextResponse } from "next/server"; + +// ADD MEMBER TASK DIVISI +export async function POST(request: Request, context: { params: { id: string } }) { + try { + const user = await funGetUserByCookies() + if (user.id == undefined) { + return NextResponse.json({ success: false, message: "Anda harus login untuk mengakses ini" }, { status: 401 }); + } + + const { id } = context.params; + const { member, idDivision } = (await request.json()); + + const data = await prisma.divisionProject.count({ + where: { + id: id, + }, + }); + + console.log(member, idDivision) + + if (data == 0) { + return NextResponse.json( + { + success: false, + message: "Tambah anggota tugas gagal, data tugas tidak ditemukan", + }, + { status: 404 } + ); + } + + if (member.length > 0) { + const dataMember = member.map((v: any) => ({ + ..._.omit(v, ["idUser", "name"]), + idDivision: idDivision, + idProject: id, + idUser: v.idUser, + })) + + const insertMember = await prisma.divisionProjectMember.createMany({ + data: dataMember + }) + } + + + return NextResponse.json( + { + success: true, + message: "Berhasil menambahkan anggota tugas", + }, + { status: 200 } + ); + } catch (error) { + console.log(error); + return NextResponse.json({ success: false, message: "Gagal menambah anggota tugas, coba lagi nanti", reason: (error as Error).message, }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/task/[id]/route.ts b/src/app/api/task/[id]/route.ts index 0817ebe..b68849f 100644 --- a/src/app/api/task/[id]/route.ts +++ b/src/app/api/task/[id]/route.ts @@ -108,6 +108,7 @@ export async function GET(request: Request, context: { params: { id: string } }) }, select: { id: true, + idUser: true, User: { select: { name: true, @@ -134,4 +135,156 @@ export async function GET(request: Request, context: { params: { id: string } }) console.log(error); return NextResponse.json({ success: false, message: "Gagal mendapatkan tugas divisi, coba lagi nanti", reason: (error as Error).message, }, { status: 500 }); } -} \ No newline at end of file +} + + +// CREATE NEW DETAIL TASK DIVISI +export async function POST(request: Request, context: { params: { id: string } }) { + try { + const user = await funGetUserByCookies() + if (user.id == undefined) { + return NextResponse.json({ success: false, message: "Anda harus login untuk mengakses ini" }, { status: 401 }); + } + + const { id } = context.params; + const { title, dateStart, dateEnd, idDivision } = (await request.json()); + const data = await prisma.divisionProject.count({ + where: { + id: id, + }, + }); + + if (data == 0) { + return NextResponse.json( + { + success: false, + message: "Tambah detail tugas gagal, data tugas tidak ditemukan", + }, + { status: 404 } + ); + } + + const create = await prisma.divisionProjectTask.create({ + data: { + idProject: id, + idDivision, + title, + dateStart: new Date(moment(dateStart).format('YYYY-MM-DD')), + dateEnd: new Date(moment(dateEnd).format('YYYY-MM-DD')), + }, + }); + + return NextResponse.json( + { + success: true, + message: "Detail tugas berhasil ditambahkan", + data, + }, + { status: 200 } + ); + } catch (error) { + console.log(error); + return NextResponse.json({ success: false, message: "Gagal mengedit detail tugas, coba lagi nanti", reason: (error as Error).message, }, { status: 500 }); + } +} + + + +// PEMBATALAN TASK DIVISI +export async function DELETE(request: Request, context: { params: { id: string } }) { + try { + const user = await funGetUserByCookies() + if (user.id == undefined) { + return NextResponse.json({ success: false, message: "Anda harus login untuk mengakses ini" }, { status: 401 }); + } + + const { id } = context.params; + const { reason } = (await request.json()); + const data = await prisma.divisionProject.count({ + where: { + id: id, + }, + }); + + if (data == 0) { + return NextResponse.json( + { + success: false, + message: "Pembatalan tugas gagal, data tugas tidak ditemukan", + }, + { status: 404 } + ); + } + + const update = await prisma.divisionProject.update({ + where: { + id + }, + data: { + reason: reason, + status: 3 + } + }); + + return NextResponse.json( + { + success: true, + message: "Tugas berhasil dibatalkan", + }, + { status: 200 } + ); + } catch (error) { + console.log(error); + return NextResponse.json({ success: false, message: "Gagal membatalkan tugas, coba lagi nanti", reason: (error as Error).message, }, { status: 500 }); + } +} + + +// EDIT TASK DIVISI +export async function PUT(request: Request, context: { params: { id: string } }) { + try { + const user = await funGetUserByCookies() + if (user.id == undefined) { + return NextResponse.json({ success: false, message: "Anda harus login untuk mengakses ini" }, { status: 401 }); + } + + const { id } = context.params; + const { title } = (await request.json()); + const data = await prisma.divisionProject.count({ + where: { + id: id, + }, + }); + + if (data == 0) { + return NextResponse.json( + { + success: false, + message: "Tugas gagal diedit, data tugas tidak ditemukan", + }, + { status: 404 } + ); + } + + const update = await prisma.divisionProject.update({ + where: { + id + }, + data: { + title + } + }); + + return NextResponse.json( + { + success: true, + message: "Tugas berhasil diedit", + }, + { status: 200 } + ); + } catch (error) { + console.log(error); + return NextResponse.json({ success: false, message: "Gagal mengedit tugas, coba lagi nanti", reason: (error as Error).message, }, { status: 500 }); + } +} + diff --git a/src/app/api/task/route.ts b/src/app/api/task/route.ts index 558cabb..7232356 100644 --- a/src/app/api/task/route.ts +++ b/src/app/api/task/route.ts @@ -78,6 +78,7 @@ export async function GET(request: Request) { } +// CREATE PROJECT TASK DIVISION export async function POST(request: Request) { try { const user = await funGetUserByCookies() diff --git a/src/module/task/index.ts b/src/module/task/index.ts index afbcfd7..159e6b9 100644 --- a/src/module/task/index.ts +++ b/src/module/task/index.ts @@ -1,3 +1,6 @@ +import AddDetailTask from "./ui/add_detail_task"; +import AddMemberDetailTask from "./ui/add_member_detail_task"; +import CancelTask from "./ui/cancel_task"; import ViewDateEndTask from "./ui/create_date_end_task"; import CreateTask from "./ui/create_task"; import CreateUsersProject from "./ui/create_users_project"; @@ -6,6 +9,7 @@ import ListFileDetailTask from "./ui/detail_list_file_task"; import ListTugasDetailTask from "./ui/detail_list_tugas_task"; import ProgressDetailTask from "./ui/detail_progress_task"; import EditDetailTask from "./ui/edit_detail_task"; +import EditTask from "./ui/edit_task"; import FileSave from "./ui/file_save"; import NavbarDetailDivisionTask from "./ui/navbar_detail_division_task"; import NavbarDivisionTask from "./ui/navbar_division_task"; @@ -22,4 +26,8 @@ export { ListTugasDetailTask } export { ProgressDetailTask } export { ListFileDetailTask } export { ListAnggotaDetailTask } -export { EditDetailTask } \ No newline at end of file +export { EditDetailTask } +export { AddDetailTask } +export { AddMemberDetailTask } +export { CancelTask } +export { EditTask } \ No newline at end of file diff --git a/src/module/task/lib/api_task.ts b/src/module/task/lib/api_task.ts index 5aec7e2..959fe28 100644 --- a/src/module/task/lib/api_task.ts +++ b/src/module/task/lib/api_task.ts @@ -1,4 +1,4 @@ -import { IFormDateTask, IFormTaskDivision } from "./type_task"; +import { IFormAddDetailTask, IFormAddMemberTask, IFormDateTask, IFormTaskDivision } from "./type_task"; export const funGetAllTask = async (path?: string) => { const response = await fetch(`/api/task${(path) ? path : ''}`, { next: { tags: ['task'] } }); @@ -64,4 +64,52 @@ export const funEditDetailTask = async (path: string, data: IFormDateTask) => { body: JSON.stringify(data), }); return await response.json().catch(() => null); +}; + + +export const funCreateDetailTask = async (path: string, data: IFormAddDetailTask) => { + const response = await fetch(`/api/task/${path}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }); + return await response.json().catch(() => null); +}; + +export const funAddMemberTask = async (path: string, data: IFormAddMemberTask) => { + const response = await fetch(`/api/task/${path}/member`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }); + return await response.json().catch(() => null); +}; + + +export const funCancelTask = async (path: string, data: { reason: string }) => { + const response = await fetch(`/api/task/${path}`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }); + return await response.json().catch(() => null); +}; + + + +export const funEditTask = async (path: string, data: { title: string }) => { + const response = await fetch(`/api/task/${path}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }); + return await response.json().catch(() => null); }; \ No newline at end of file diff --git a/src/module/task/lib/type_task.ts b/src/module/task/lib/type_task.ts index 1cfa5bd..6c89dce 100644 --- a/src/module/task/lib/type_task.ts +++ b/src/module/task/lib/type_task.ts @@ -19,6 +19,19 @@ export interface IFormDateTask { title: string } +export interface IFormAddDetailTask { + dateStart: Date, + dateEnd: Date, + title: string + idDivision: string +} + + +export interface IFormAddMemberTask { + idDivision: string + member: IFormMemberTask[] | [] +} + export interface IFormTaskDivision { idDivision: string @@ -44,6 +57,7 @@ export interface IDataListTaskDivision { export interface IDataMemberTaskDivision { id: string + idUser: string name: string email: string } diff --git a/src/module/task/ui/add_detail_task.tsx b/src/module/task/ui/add_detail_task.tsx new file mode 100644 index 0000000..394ec3b --- /dev/null +++ b/src/module/task/ui/add_detail_task.tsx @@ -0,0 +1,147 @@ +"use client"; +import { LayoutNavbarNew, WARNA } from "@/module/_global"; +import { + Avatar, + Box, + Button, + Flex, + Group, + Input, + SimpleGrid, + Stack, + Text, +} from "@mantine/core"; +import React, { useState } from "react"; +import { DatePicker } from "@mantine/dates"; +import { useParams, useRouter } from "next/navigation"; +import toast from "react-hot-toast"; +import { IFormDateTask } from "../lib/type_task"; +import moment from "moment"; +import { funCreateDetailTask } from "../lib/api_task"; +import LayoutModal from "@/module/_global/layout/layout_modal"; + + +export default function AddDetailTask() { + const [value, setValue] = useState<[Date | null, Date | null]>([null, null]); + const router = useRouter() + const [title, setTitle] = useState("") + const [openModal, setOpenModal] = useState(false) + const param = useParams<{ id: string, detail: string }>() + + function onVerification() { + if (value[0] == null || value[1] == null) + return toast.error("Error! harus memilih tanggal") + + if (title == "") + return toast.error("Error! harus memasukkan judul tugas") + + setOpenModal(true) + } + + async function onSubmit() { + try { + const res = await funCreateDetailTask(param.detail, { + title, + dateStart: (value[0] != null) ? value[0] : new Date, + dateEnd: (value[1] != null) ? value[1] : new Date, + idDivision: param.id + }) + + if (res.success) { + toast.success(res.message) + setOpenModal(false) + router.push(`/division/${param.id}/task/${param.detail}`) + } else { + toast.error(res.message) + } + } catch (error) { + console.log(error) + toast.error("Gagal menambahkan tugas, coba lagi nanti") + } + } + + + + return ( + + + + + + + + + Tanggal Mulai + + {value[0] ? `${moment(value[0]).format('DD-MM-YYYY')}` : ""} + + + + Tanggal Berakhir + + {value[1] ? `${moment(value[1]).format('DD-MM-YYYY')}` : ""} + + + + + setTitle(e.target.value)} + /> + + + + + + + + setOpenModal(false)} + description="Apakah Anda yakin ingin menambahkan tugas?" + onYes={(val) => { + if (val) { + onSubmit() + } + setOpenModal(false) + }} /> + + ); +} diff --git a/src/module/task/ui/add_member_detail_task.tsx b/src/module/task/ui/add_member_detail_task.tsx new file mode 100644 index 0000000..aa85df3 --- /dev/null +++ b/src/module/task/ui/add_member_detail_task.tsx @@ -0,0 +1,203 @@ +"use client" +import { LayoutNavbarNew, WARNA } from "@/module/_global"; +import { funGetDivisionById, IDataMemberDivision } from "@/module/division_new"; +import { + Anchor, + Avatar, + Box, + Button, + Checkbox, + Divider, + Flex, + Group, + Stack, + Text, + TextInput, +} from "@mantine/core"; +import { useShallowEffect } from "@mantine/hooks"; +import { useParams, useRouter } from "next/navigation"; +import React, { useState } from "react"; +import toast from "react-hot-toast"; +import { FaCheck } from "react-icons/fa6"; +import { funAddMemberTask, funGetTaskDivisionById } from "../lib/api_task"; +import { IDataMemberTaskDivision } from "../lib/type_task"; +import LayoutModal from "@/module/_global/layout/layout_modal"; + +export default function AddMemberDetailTask() { + const router = useRouter() + const param = useParams<{ id: string, detail: string }>() + const [selectedFiles, setSelectedFiles] = useState([]) + const [isData, setData] = useState([]) + const [isDataMember, setDataMember] = useState([]) + const [selectAll, setSelectAll] = useState(false) + const [openModal, setOpenModal] = useState(false) + + + async function getData() { + try { + const response = await funGetDivisionById(param.id) + if (response.success) { + setData(response.data.member) + } else { + toast.error(response.message) + } + + const res = await funGetTaskDivisionById(param.detail, 'member'); + if (res.success) { + setDataMember(res.data) + } else { + toast.error(res.message); + } + } catch (error) { + console.log(error) + toast.error("Gagal mendapatkan anggota, coba lagi nanti"); + } + } + + + useShallowEffect(() => { + getData() + }, []); + + const handleFileClick = (index: number) => { + if (selectedFiles.some((i: any) => i.idUser == isData[index].idUser)) { + setSelectedFiles(selectedFiles.filter((i: any) => i.idUser != isData[index].idUser)) + } else { + setSelectedFiles([...selectedFiles, { idUser: isData[index].idUser, name: isData[index].name }]) + } + }; + + + + const handleSelectAll = () => { + setSelectAll(!selectAll); + if (!selectAll) { + for (let index = 0; index < isData.length; index++) { + if (!isDataMember.some((i: any) => i.idUser == isData[index].idUser)) { + if (!selectedFiles.some((i: any) => i.idUser == isData[index].idUser)) { + const newArr = { + idUser: isData[index].idUser, name: isData[index].name + } + setSelectedFiles((selectedFiles: any) => [...selectedFiles, newArr]) + } + } + + } + } else { + setSelectedFiles([]); + } + }; + + + function onVerifikasi() { + if (selectedFiles.length == 0) { + return toast.error("Error! silahkan pilih anggota") + } + + setOpenModal(true) + } + + async function onSubmit() { + try { + const res = await funAddMemberTask(param.detail, { idDivision: param.id, member: selectedFiles }); + if (res.success) { + toast.success(res.message) + router.back() + } else { + toast.error(res.message) + } + } catch (error) { + console.log(error) + toast.error("Gagal menambahkan anggota, coba lagi nanti"); + } + } + + + return ( + + + + {/* } + placeholder="Pencarian" + /> */} + + + Pilih Semua Anggota + + {selectAll ? : ""} + + + {isData.map((v, i) => { + const isSelected = selectedFiles.some((i: any) => i?.idUser == v.idUser); + const found = isDataMember.some((i: any) => i.idUser == v.idUser) + return ( + (!found) ? handleFileClick(i) : null}> + + + + + + {v.name} + + {(found) ? "sudah menjadi anggota" : ""} + + + + {isSelected ? : ""} + + + + + ); + })} + + + + + + + setOpenModal(false)} + description="Apakah Anda yakin ingin menambahkan anggota?" + onYes={(val) => { + if (val) { + onSubmit() + } + setOpenModal(false) + }} /> + + ); +} diff --git a/src/module/task/ui/cancel_task.tsx b/src/module/task/ui/cancel_task.tsx new file mode 100644 index 0000000..1e0b4db --- /dev/null +++ b/src/module/task/ui/cancel_task.tsx @@ -0,0 +1,87 @@ +"use client"; +import { LayoutNavbarNew, WARNA } from "@/module/_global"; +import { + Box, + Button, + Stack, + Textarea, +} from "@mantine/core"; +import React, { useState } from "react"; +import { useParams, useRouter } from "next/navigation"; +import toast from "react-hot-toast"; +import { funCancelTask } from "../lib/api_task"; +import LayoutModal from "@/module/_global/layout/layout_modal"; + + +export default function CancelTask() { + const router = useRouter() + const [alasan, setAlasan] = useState("") + const [openModal, setOpenModal] = useState(false) + const param = useParams<{ id: string, detail: string }>() + + function onVerification() { + if (alasan == "") + return toast.error("Error! harus memasukkan alasan pembatalan tugas") + + setOpenModal(true) + } + + async function onSubmit() { + try { + const res = await funCancelTask(param.detail, { reason: alasan }) + if (res.success) { + toast.success(res.message) + router.push("./") + } else { + toast.error(res.message) + } + } catch (error) { + console.log(error) + toast.error("Gagal membatalkan tugas, coba lagi nanti") + } + } + + + + return ( + + + + +