From 42f6257d03b55a20226a6f6c179691b70b06b4e0 Mon Sep 17 00:00:00 2001 From: amel Date: Tue, 27 May 2025 12:20:47 +0800 Subject: [PATCH] upd: task division Deskripsi: - tambah task division - hapus task division yg telah dibatalkan - akses user sesuai dengan role No Issuese --- .../(fitur-division)/task/[detail]/cancel.tsx | 2 +- .../(fitur-division)/task/[detail]/index.tsx | 3 +- .../[id]/(fitur-division)/task/create.tsx | 225 ++++++++++++++++-- .../(fitur-division)/task/create/member.tsx | 139 +++++++++++ .../(fitur-division)/task/create/task.tsx | 155 ++++++++++++ .../[id]/(fitur-division)/task/index.tsx | 4 +- components/task/headerTaskDetail.tsx | 158 +++++++++--- components/task/headerTaskList.tsx | 36 ++- lib/api.ts | 14 ++ 9 files changed, 672 insertions(+), 64 deletions(-) create mode 100644 app/(application)/division/[id]/(fitur-division)/task/create/member.tsx create mode 100644 app/(application)/division/[id]/(fitur-division)/task/create/task.tsx diff --git a/app/(application)/division/[id]/(fitur-division)/task/[detail]/cancel.tsx b/app/(application)/division/[id]/(fitur-division)/task/[detail]/cancel.tsx index fe7070d..181bfc1 100644 --- a/app/(application)/division/[id]/(fitur-division)/task/[detail]/cancel.tsx +++ b/app/(application)/division/[id]/(fitur-division)/task/[detail]/cancel.tsx @@ -14,7 +14,7 @@ export default function TaskDivisionCancel() { const { id, detail } = useLocalSearchParams<{ id: string; detail: string }>(); const { token, decryptToken } = useAuthSession(); const dispatch = useDispatch(); - const update = useSelector((state: any) => state.projectUpdate); + const update = useSelector((state: any) => state.taskUpdate); const [reason, setReason] = useState(""); const [error, setError] = useState(false); const [disable, setDisable] = useState(false); diff --git a/app/(application)/division/[id]/(fitur-division)/task/[detail]/index.tsx b/app/(application)/division/[id]/(fitur-division)/task/[detail]/index.tsx index 364a219..5d5f5ae 100644 --- a/app/(application)/division/[id]/(fitur-division)/task/[detail]/index.tsx +++ b/app/(application)/division/[id]/(fitur-division)/task/[detail]/index.tsx @@ -50,7 +50,6 @@ export default function DetailTaskDivision() { handleLoad() }, [update.progress, update.data]) - return ( { router.back() }} />, headerTitle: loading ? 'Loading...' : data?.title, headerTitleAlign: 'center', - headerRight: () => , + headerRight: () => , }} /> diff --git a/app/(application)/division/[id]/(fitur-division)/task/create.tsx b/app/(application)/division/[id]/(fitur-division)/task/create.tsx index 909a12f..cc96bbb 100644 --- a/app/(application)/division/[id]/(fitur-division)/task/create.tsx +++ b/app/(application)/division/[id]/(fitur-division)/task/create.tsx @@ -1,47 +1,220 @@ +import BorderBottomItem from "@/components/borderBottomItem"; import ButtonBackHeader from "@/components/buttonBackHeader"; import ButtonSaveHeader from "@/components/buttonSaveHeader"; import ButtonSelect from "@/components/buttonSelect"; +import DrawerBottom from "@/components/drawerBottom"; +import ImageUser from "@/components/imageNew"; import { InputForm } from "@/components/inputForm"; +import MenuItemRow from "@/components/menuItemRow"; +import ModalSelect from "@/components/modalSelect"; +import SectionListAddTask from "@/components/project/sectionListAddTask"; import Styles from "@/constants/Styles"; +import { apiCreateTask } from "@/lib/api"; +import { setMemberChoose } from "@/lib/memberChoose"; +import { setTaskCreate } from "@/lib/taskCreate"; +import { setUpdateTask } from "@/lib/taskUpdate"; +import { useAuthSession } from "@/providers/AuthProvider"; +import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"; +import * as DocumentPicker from "expo-document-picker"; import { router, Stack, useLocalSearchParams } from "expo-router"; -import { SafeAreaView, ScrollView, ToastAndroid, View } from "react-native"; +import { useEffect, useState } from "react"; +import { SafeAreaView, ScrollView, Text, ToastAndroid, View } from "react-native"; +import { useDispatch, useSelector } from "react-redux"; + export default function CreateTaskDivision() { - const { id } = useLocalSearchParams() + const { id } = useLocalSearchParams(); + const { token, decryptToken } = useAuthSession(); + const dispatch = useDispatch(); + const [isSelect, setSelect] = useState(false); + const [valChoose, setValChoose] = useState(""); + const entitiesMember = useSelector((state: any) => state.memberChoose); + const taskCreate = useSelector((state: any) => state.taskCreate); + const update = useSelector((state: any) => state.taskUpdate) + const [fileForm, setFileForm] = useState([]) + const [indexDelFile, setIndexDelFile] = useState(0) + const [title, setTitle] = useState('') + const [error, setError] = useState(false); + const [isModal, setModal] = useState(false) + let hitung = 0; + + useEffect(() => { + if (hitung == 0) { + dispatch(setTaskCreate([])); + dispatch(setMemberChoose([])); + } + hitung++; + }, []); + + + const pickDocumentAsync = async () => { + let result = await DocumentPicker.getDocumentAsync({ + type: ["*/*"], + multiple: false + }); + if (!result.canceled) { + if (result.assets[0].uri) { + setFileForm([...fileForm, result.assets[0]]) + } + } + }; + + function deleteFile(index: number) { + setFileForm([...fileForm.filter((val, i) => i !== index)]) + setModal(false) + } + + + function handleBack() { + dispatch(setTaskCreate([])); + dispatch(setMemberChoose([])); + router.back(); + } + + async function handleCreate() { + try { + const hasil = await decryptToken(String(token?.current)) + const fd = new FormData() + + for (let i = 0; i < fileForm.length; i++) { + fd.append(`file${i}`, { + uri: fileForm[i].uri, + type: 'application/octet-stream', + name: fileForm[i].name, + } as any); + } + + fd.append("data", JSON.stringify( + { user: hasil, task: taskCreate, member: entitiesMember, title, idDivision: id } + )) + + const response = await apiCreateTask(fd) + if (response.success) { + dispatch(setUpdateTask({ ...update, data: !update.data })) + ToastAndroid.show('Berhasil menambahkan data', ToastAndroid.SHORT) + handleBack() + } else { + ToastAndroid.show(response.message, ToastAndroid.SHORT) + } + } catch (error) { + console.error(error) + } + } + return ( { router.back() }} />, + headerLeft: () => ( + { + handleBack(); + }} + /> + ), headerTitle: `Tambah Tugas`, - headerTitleAlign: 'center', - headerRight: () => { - ToastAndroid.show('Berhasil menambah data', ToastAndroid.SHORT) - router.push('../task?status=0') - }} /> + headerTitleAlign: "center", + headerRight: () => ( + { handleCreate() }} + /> + ), }} /> - - - - - {/* { - AlertKonfirmasi({ - title: 'Konfirmasi', - desc: 'Apakah anda yakin ingin menambahkan data?', - onPress: () => { - ToastAndroid.show('Berhasil menambahkan data', ToastAndroid.SHORT) - router.push('../task?status=0') - } - }) - }} /> */} + { + setTitle(val); + val == "" || val == "null" ? setError(true) : setError(false); + }} + error={error} + errorText="Judul Tugas tidak boleh kosong" + /> + { router.push(`/division/${id}/task/create/task`); }} /> + + { router.push(`/division/${id}/task/create/member`); }} /> + + { + fileForm.length > 0 && ( + + File + + { + fileForm.map((item, index) => ( + } + title={item.name} + titleWeight="normal" + onPress={() => { setIndexDelFile(index); setModal(true) }} + /> + )) + } + + + ) + } + {entitiesMember.length > 0 && ( + + + Anggota + Total {entitiesMember.length} Anggota + + + + {entitiesMember.map( + (item: { img: any; name: any }, index: any) => { + return ( + + } + title={item.name} + /> + ); + } + )} + + + )} + + + + + } + title="Hapus" + onPress={() => { deleteFile(indexDelFile) }} + /> + + + + { }} + title={"Pilih Anggota"} + open={isSelect} + idParent={''} + valChoose={valChoose} + /> - ) -} \ No newline at end of file + ); +} diff --git a/app/(application)/division/[id]/(fitur-division)/task/create/member.tsx b/app/(application)/division/[id]/(fitur-division)/task/create/member.tsx new file mode 100644 index 0000000..b81db06 --- /dev/null +++ b/app/(application)/division/[id]/(fitur-division)/task/create/member.tsx @@ -0,0 +1,139 @@ +import ButtonBackHeader from "@/components/buttonBackHeader"; +import ButtonSaveHeader from "@/components/buttonSaveHeader"; +import ImageUser from "@/components/imageNew"; +import ImageWithLabel from "@/components/imageWithLabel"; +import InputSearch from "@/components/inputSearch"; +import Styles from "@/constants/Styles"; +import { apiGetDivisionMember } from "@/lib/api"; +import { setMemberChoose } from "@/lib/memberChoose"; +import { useAuthSession } from "@/providers/AuthProvider"; +import { AntDesign } from "@expo/vector-icons"; +import { router, Stack, useLocalSearchParams } from "expo-router"; +import { useEffect, useState } from "react"; +import { Pressable, SafeAreaView, ScrollView, Text, ToastAndroid, View } from "react-native"; +import { useDispatch, useSelector } from "react-redux"; + +type Props = { + idUser: string, + name: string, + img: string +} + +export default function AddMemberCreateTask() { + const dispatch = useDispatch() + const { token, decryptToken } = useAuthSession() + const { id } = useLocalSearchParams<{ id: string, detail: string }>() + const [dataOld, setDataOld] = useState([]) + const [data, setData] = useState([]) + const [selectMember, setSelectMember] = useState([]) + const [search, setSearch] = useState('') + const entitiesMember = useSelector((state: any) => state.memberChoose) + + async function handleLoadMemberDivision() { + const hasil = await decryptToken(String(token?.current)) + const responMemberDivision = await apiGetDivisionMember({ id: id, user: hasil, search: search }) + setData(responMemberDivision.data) + if (entitiesMember.length > 0) { + setSelectMember(entitiesMember) + } + } + + useEffect(() => { + handleLoadMemberDivision() + }, [search]); + + function onChoose(val: string, label: string, img?: string) { + if (selectMember.some((i: any) => i.idUser == val)) { + setSelectMember(selectMember.filter((i: any) => i.idUser != val)) + } else { + setSelectMember([...selectMember, { idUser: val, name: label, img }]) + } + } + + async function handleAddMember() { + try { + dispatch(setMemberChoose(selectMember)) + router.back() + } catch (error) { + console.error(error) + ToastAndroid.show('Gagal menambahkan anggota', ToastAndroid.SHORT) + } + } + + + return ( + + { router.back() }} />, + headerTitle: 'Pilih Anggota', + headerTitleAlign: 'center', + headerRight: () => ( + 0 ? false : true} + onPress={() => { + handleAddMember() + }} + /> + ) + }} + /> + + setSearch(val)} value={search} /> + + { + selectMember.length > 0 + ? + + + { + selectMember.map((item: any, index: any) => ( + onChoose(item.idUser, item.name, item.img)} + /> + )) + } + + + + : + Tidak ada member yang dipilih + } + + + { + data.length > 0 ? + data.map((item: any, index: any) => { + return ( + { + onChoose(item.idUser, item.name, item.img) + }} + > + + + + {item.name} + + + { + selectMember.some((i: any) => i.idUser == item.idUser) && + } + + ) + } + ) + : + Tidak ada data + } + + + + ) +} \ No newline at end of file diff --git a/app/(application)/division/[id]/(fitur-division)/task/create/task.tsx b/app/(application)/division/[id]/(fitur-division)/task/create/task.tsx new file mode 100644 index 0000000..69c1c2c --- /dev/null +++ b/app/(application)/division/[id]/(fitur-division)/task/create/task.tsx @@ -0,0 +1,155 @@ +import ButtonBackHeader from "@/components/buttonBackHeader"; +import ButtonSaveHeader from "@/components/buttonSaveHeader"; +import { InputForm } from "@/components/inputForm"; +import Styles from "@/constants/Styles"; +import { setTaskCreate } from "@/lib/taskCreate"; +import dayjs from "dayjs"; +import { router, Stack } from "expo-router"; +import { useEffect, useState } from "react"; +import { + SafeAreaView, + ScrollView, + Text, + View +} from "react-native"; +import DateTimePicker, { + DateType +} from "react-native-ui-datepicker"; +import { useDispatch, useSelector } from "react-redux"; + +export default function CreateTaskAddTugas() { + const dispatch = useDispatch() + const [disable, setDisable] = useState(true); + const [range, setRange] = useState<{ + startDate: DateType; + endDate: DateType; + }>({ startDate: undefined, endDate: undefined }); + const [error, setError] = useState({ + startDate: false, + endDate: false, + title: false, + }) + const [title, setTitle] = useState(''); + const taskCreate = useSelector((state: any) => state.taskCreate) + + const from = range.startDate + ? dayjs(range.startDate).format("DD-MM-YYYY") + : ""; + const to = range.endDate ? dayjs(range.endDate).format("DD-MM-YYYY") : ""; + + function checkAll() { + if (from == "" || to == "" || title == "" || title == "null" || error.startDate || error.endDate || error.title) { + setDisable(true) + } else { + setDisable(false) + } + } + + function onValidation(cat: string, val: string) { + if (cat == "title") { + setTitle(val) + if (val == "" || val == "null") { + setError(error => ({ ...error, title: true })) + } else { + setError(error => ({ ...error, title: false })) + } + } + } + + useEffect(() => { + checkAll() + }, [from, to, title, error]) + + async function handleCreate() { + try { + dispatch(setTaskCreate([...taskCreate, { + title: title, + dateStart: from, + dateEnd: to, + dateStartFix: dayjs(range.startDate).format("YYYY-MM-DD"), + dateEndFix: dayjs(range.endDate).format("YYYY-MM-DD"), + }])) + router.back(); + } catch (error) { + console.error(error); + } + } + + return ( + + ( + { + router.back(); + }} + /> + ), + headerTitle: "Tambah Tugas", + headerTitleAlign: "center", + headerRight: () => ( + { handleCreate() }} + /> + ), + }} + /> + + + + setRange(param)} + styles={{ + selected: Styles.selectedDate, + selected_label: Styles.cWhite, + range_fill: Styles.selectRangeDate, + }} + /> + + + + + + Tanggal Mulai * + + + {from} + + + + + Tanggal Berakhir * + + + {to} + + + + { + (error.endDate || error.startDate) && Tanggal tidak boleh kosong + } + + { + onValidation("title", e) + }} + /> + + + + ); +} diff --git a/app/(application)/division/[id]/(fitur-division)/task/index.tsx b/app/(application)/division/[id]/(fitur-division)/task/index.tsx index b210724..e53bb81 100644 --- a/app/(application)/division/[id]/(fitur-division)/task/index.tsx +++ b/app/(application)/division/[id]/(fitur-division)/task/index.tsx @@ -16,6 +16,7 @@ import { import { router, useLocalSearchParams } from "expo-router"; import { useEffect, useState } from "react"; import { Pressable, SafeAreaView, ScrollView, Text, View } from "react-native"; +import { useSelector } from "react-redux"; type Props = { id: string; @@ -32,6 +33,7 @@ export default function ListTask() { const { token, decryptToken } = useAuthSession(); const [data, setData] = useState([]); const [search, setSearch] = useState(""); + const update = useSelector((state: any) => state.taskUpdate) async function handleLoad() { try { @@ -50,7 +52,7 @@ export default function ListTask() { useEffect(() => { handleLoad(); - }, [status, search]); + }, [status, search, update.data]); return ( diff --git a/components/task/headerTaskDetail.tsx b/components/task/headerTaskDetail.tsx index 04a7b17..bbf507b 100644 --- a/components/task/headerTaskDetail.tsx +++ b/components/task/headerTaskDetail.tsx @@ -1,69 +1,165 @@ import Styles from "@/constants/Styles" -import { AntDesign, MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons" +import { apiDeleteTask, apiGetDivisionOneFeature } from "@/lib/api" +import { setUpdateTask } from "@/lib/taskUpdate" +import { useAuthSession } from "@/providers/AuthProvider" +import { AntDesign, Ionicons, MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons" import { router } from "expo-router" -import { useState } from "react" -import { View } from "react-native" +import { useEffect, useState } from "react" +import { ToastAndroid, View } from "react-native" +import { useDispatch, useSelector } from "react-redux" +import AlertKonfirmasi from "../alertKonfirmasi" import ButtonMenuHeader from "../buttonMenuHeader" import DrawerBottom from "../drawerBottom" import MenuItemRow from "../menuItemRow" type Props = { id: string | string[] + division: string + status: number | undefined } -export default function HeaderRightTaskDetail({ id }: Props) { +export default function HeaderRightTaskDetail({ id, division, status }: Props) { + const { token, decryptToken } = useAuthSession() const [isVisible, setVisible] = useState(false) + const entityUser = useSelector((state: any) => state.user); + const [isMemberDivision, setIsMemberDivision] = useState(false); + const [isAdminDivision, setIsAdminDivision] = useState(false); + const dispatch = useDispatch() + const update = useSelector((state: any) => state.taskUpdate) + + async function handleCheckMember() { + try { + const hasil = await decryptToken(String(token?.current)); + const response = await apiGetDivisionOneFeature({ + id: division, + user: hasil, + cat: "check-member", + }); + + setIsMemberDivision(response.data); + + const response2 = await apiGetDivisionOneFeature({ + id: division, + user: hasil, + cat: "check-admin", + }); + setIsAdminDivision(response2.data); + } catch (error) { + console.error(error); + } + } + + useEffect(() => { + handleCheckMember() + }, []) + + async function handleDelete() { + try { + const hasil = await decryptToken(String(token?.current)) + const response = await apiDeleteTask({ user: hasil }, String(id)) + if (response.success) { + dispatch(setUpdateTask({ ...update, data: !update.data })) + ToastAndroid.show('Berhasil menghapus tugas', ToastAndroid.SHORT) + router.back() + } else { + ToastAndroid.show(response.message, ToastAndroid.SHORT) + } + } catch (error) { + console.error(error) + } finally { + setVisible(false) + } + } return ( <> - { setVisible(true) }} /> + { + (entityUser.role == "user" || entityUser.role == "coadmin") && !isMemberDivision + ? <> + : + { setVisible(true) }} /> + } } title="Tambah Tugas" onPress={() => { + if (status == 3) return setVisible(false) router.push(`./${id}/add-task`) }} + disabled={status == 3} /> } title="Tambah File" onPress={() => { + if (status == 3) return setVisible(false) router.push(`./${id}/add-file`) }} + disabled={status == 3} /> + { + ( (entityUser.role != "user" && entityUser.role != "coadmin") || isAdminDivision) + && + } + title="Tambah Anggota" + onPress={() => { + if (status == 3) return + setVisible(false) + router.push(`./${id}/add-member`) + }} + disabled={status == 3} + /> - } - title="Tambah Anggota" - onPress={() => { - setVisible(false) - router.push(`./${id}/add-member`) - }} - /> + } - - } - title="Edit" - onPress={() => { - setVisible(false) - router.push(`./${id}/edit`) - }} - /> - } - title="Batal" - onPress={() => { - setVisible(false) - router.push(`./${id}/cancel`) - }} - /> - + + { + ( (entityUser.role != "user" && entityUser.role != "coadmin") || isAdminDivision ) + && + + } + title="Edit" + onPress={() => { + if (status == 3) return + setVisible(false) + router.push(`./${id}/edit`) + }} + disabled={status == 3} + /> + { + status == 3 + ? + } + title="Hapus" + onPress={() => { + AlertKonfirmasi({ + title: 'Konfirmasi', + desc: 'Apakah Anda yakin ingin menghapus tugas ini? Tugas yang dihapus tidak dapat dikembalikan', + onPress: () => { handleDelete() } + }) + }} + /> + + : + } + title="Batal" + onPress={() => { + setVisible(false) + router.push(`./${id}/cancel`) + }} + /> + } + + } ) diff --git a/components/task/headerTaskList.tsx b/components/task/headerTaskList.tsx index 0936ec9..51e6c22 100644 --- a/components/task/headerTaskList.tsx +++ b/components/task/headerTaskList.tsx @@ -1,17 +1,47 @@ import Styles from "@/constants/Styles" +import { apiGetDivisionOneFeature } from "@/lib/api" +import { useAuthSession } from "@/providers/AuthProvider" import { AntDesign } from "@expo/vector-icons" -import { useState } from "react" +import { router, useLocalSearchParams } from "expo-router" +import { useEffect, useState } from "react" import { View } from "react-native" +import { useSelector } from "react-redux" import ButtonMenuHeader from "../buttonMenuHeader" import DrawerBottom from "../drawerBottom" import MenuItemRow from "../menuItemRow" -import { router } from "expo-router" export default function HeaderRightTaskList() { const [isVisible, setVisible] = useState(false) + const [isAdminDivision, setIsAdminDivision] = useState(false); + const { token, decryptToken } = useAuthSession() + const { id } = useLocalSearchParams<{ id: string }>(); + const entityUser = useSelector((state: any) => state.user); + + async function handleCheckAdmin() { + try { + const hasil = await decryptToken(String(token?.current)); + const response = await apiGetDivisionOneFeature({ + id, + user: hasil, + cat: "check-admin", + }); + setIsAdminDivision(response.data); + } catch (error) { + console.error(error); + } + } + + useEffect(() => { + handleCheckAdmin() + }, []) + + return ( <> - { setVisible(true) }} /> + { + (entityUser.role != "user" && entityUser.role != "coadmin") || isAdminDivision + ? { setVisible(true) }} /> : <> + } { const response = await api.post(`/mobile/task/${id}/member`, data) return response.data; +}; + +export const apiCreateTask = async (data: FormData) => { + const response = await api.post(`/mobile/task`, data, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }) + return response.data; +}; + +export const apiDeleteTask = async (data: { user: string }, id: string) => { + const response = await api.delete(`/mobile/task/${id}/lainnya`, { data }) + return response.data; }; \ No newline at end of file