From 72fa18565de4a3a931edf319800310b437ca0d0c Mon Sep 17 00:00:00 2001 From: amaliadwiy Date: Wed, 20 Aug 2025 15:17:10 +0800 Subject: [PATCH 1/2] upd: fitur baru project Deskripsi: - tampilan list detail tugas project - tampilan tambah detail tugas project - tampilan edit detail tugas project - tampilan form tambah data project > detail tugas - integrasi api get list detail tugas project - integrasi api tambah detail tugas project - integrasi api edit detail tugas project - integrasi api tambah data project > detail tugas No Issues --- app/(application)/project/[id]/add-task.tsx | 70 +++++++-- app/(application)/project/create.tsx | 6 - app/(application)/project/create/task.tsx | 61 +++++++- app/(application)/project/update/[detail].tsx | 87 ++++++++++- components/modalFloat.tsx | 4 +- .../project/modalAddDetailTugasProject.tsx | 135 ++++++++++++++++++ .../project/modalListDetailTugasProject.tsx | 2 +- constants/Styles.ts | 3 + lib/api.ts | 4 +- lib/fun_formatDateOnly.ts | 9 ++ lib/fun_getDatesInRange.ts | 14 ++ 11 files changed, 360 insertions(+), 35 deletions(-) create mode 100644 components/project/modalAddDetailTugasProject.tsx create mode 100644 lib/fun_formatDateOnly.ts create mode 100644 lib/fun_getDatesInRange.ts diff --git a/app/(application)/project/[id]/add-task.tsx b/app/(application)/project/[id]/add-task.tsx index 78e8f3c..4f2fc56 100644 --- a/app/(application)/project/[id]/add-task.tsx +++ b/app/(application)/project/[id]/add-task.tsx @@ -1,16 +1,19 @@ import ButtonBackHeader from "@/components/buttonBackHeader"; import ButtonSaveHeader from "@/components/buttonSaveHeader"; import { InputForm } from "@/components/inputForm"; +import ModalAddDetailTugasProject from "@/components/project/modalAddDetailTugasProject"; import Text from "@/components/Text"; import Styles from "@/constants/Styles"; import { apiCreateProjectTask } from "@/lib/api"; +import { formatDateOnly } from "@/lib/fun_formatDateOnly"; +import { getDatesInRange } from "@/lib/fun_getDatesInRange"; import { setUpdateProject } from "@/lib/projectUpdate"; import { useAuthSession } from "@/providers/AuthProvider"; import { useHeaderHeight } from '@react-navigation/elements'; -import dayjs from "dayjs"; import { router, Stack, useLocalSearchParams } from "expo-router"; import 'intl'; import 'intl/locale-data/jsonp/id'; +import moment from "moment"; import { useEffect, useState } from "react"; import { KeyboardAvoidingView, @@ -31,6 +34,8 @@ export default function ProjectAddTask() { const { token, decryptToken } = useAuthSession() const dispatch = useDispatch() const update = useSelector((state: any) => state.projectUpdate) + const [dataDetail, setDataDetail] = useState([]) + const [modalDetail, setModalDetail] = useState(false) const { id } = useLocalSearchParams<{ id: string }>(); const [disable, setDisable] = useState(true); const [range, setRange] = useState<{ @@ -44,11 +49,10 @@ export default function ProjectAddTask() { }) const [title, setTitle] = useState(''); const [loading, setLoading] = useState(false) + const [dsbButton, setDsbButton] = useState(true) - const from = range.startDate - ? dayjs(range.startDate).format("DD-MM-YYYY") - : ""; - const to = range.endDate ? dayjs(range.endDate).format("DD-MM-YYYY") : ""; + const from = formatDateOnly(range.startDate); + const to = formatDateOnly(range.endDate); function checkAll() { if (from == "" || to == "" || title == "" || title == "null" || error.startDate || error.endDate || error.title) { @@ -69,15 +73,50 @@ export default function ProjectAddTask() { } } + function checkButton() { + if (range.startDate == null || range.endDate == null || range.startDate == undefined || range.endDate == undefined) { + setDsbButton(true) + setDataDetail([]) + } else { + setDsbButton(false) + const awal = formatDateOnly(range.startDate, "YYYY-MM-DD") + const akhir = formatDateOnly(range.endDate, "YYYY-MM-DD") + const datanya = getDatesInRange(awal, akhir) + setDataDetail(datanya.map((item: any) => ({ + date: item, + timeStart: null, + timeEnd: null, + }))) + } + } + useEffect(() => { checkAll() }, [from, to, title, error]) + useEffect(() => { + checkButton() + }, [range]) + + async function handleCreate() { try { setLoading(true) + const dataDetailFix = dataDetail.map((item: any) => ({ + date: moment(item.date, "DD-MM-YYYY").format("YYYY-MM-DD"), + timeStart: item.timeStart, + timeEnd: item.timeEnd, + })) const hasil = await decryptToken(String(token?.current)); - const response = await apiCreateProjectTask({ data: { name: title, dateStart: dayjs(range.startDate).format("YYYY-MM-DD"), dateEnd: dayjs(range.endDate).format("YYYY-MM-DD"), user: hasil }, id }); + const response = await apiCreateProjectTask({ + data: { + name: title, + dateStart: formatDateOnly(range.startDate, "YYYY-MM-DD"), + dateEnd: formatDateOnly(range.endDate, "YYYY-MM-DD"), + user: hasil, + dataDetail: dataDetailFix + }, id + }); if (response.success) { dispatch(setUpdateProject({ ...update, task: !update.task, progress: !update.progress })) Toast.show({ type: 'small', text1: 'Berhasil menambah data', }) @@ -126,7 +165,7 @@ export default function ProjectAddTask() { mode="range" startDate={range.startDate} endDate={range.endDate} - onChange={(param) => setRange(param)} + onChange={(param) => { setRange(param) }} styles={{ selected: Styles.selectedDate, selected_label: Styles.cWhite, @@ -163,9 +202,12 @@ export default function ProjectAddTask() { { (error.endDate || error.startDate) && Tanggal tidak boleh kosong } - {/* TODO */} - - Detail + { setModalDetail(true) }} + > + Detail + { + setDataDetail(data) + }} + /> ); } diff --git a/app/(application)/project/create.tsx b/app/(application)/project/create.tsx index 92aff0d..02c50a3 100644 --- a/app/(application)/project/create.tsx +++ b/app/(application)/project/create.tsx @@ -251,18 +251,12 @@ export default function CreateProject() { onPress={() => { if (entityUser.role == "supadmin" || entityUser.role == "developer") { if (chooseGroup.val != "") { - // setSelect(true); - // setValSelect("member"); router.push(`/project/create/member`); } else { Toast.show({ type: 'small', text1: "Pilih Lembaga Desa terlebih dahulu", }) } } else { router.push(`/project/create/member`); - // validationForm('group', userLogin.idGroup, userLogin.group); - // setValChoose(userLogin.idGroup) - // setSelect(true); - // setValSelect("member"); } }} error={error.member} diff --git a/app/(application)/project/create/task.tsx b/app/(application)/project/create/task.tsx index f916b4d..00bd502 100644 --- a/app/(application)/project/create/task.tsx +++ b/app/(application)/project/create/task.tsx @@ -1,18 +1,22 @@ import ButtonBackHeader from "@/components/buttonBackHeader"; import ButtonSaveHeader from "@/components/buttonSaveHeader"; import { InputForm } from "@/components/inputForm"; +import ModalAddDetailTugasProject from "@/components/project/modalAddDetailTugasProject"; import Text from "@/components/Text"; import Styles from "@/constants/Styles"; +import { formatDateOnly } from "@/lib/fun_formatDateOnly"; +import { getDatesInRange } from "@/lib/fun_getDatesInRange"; import { setTaskCreate } from "@/lib/taskCreate"; import { useHeaderHeight } from '@react-navigation/elements'; -import dayjs from "dayjs"; import { router, Stack } from "expo-router"; import 'intl'; import 'intl/locale-data/jsonp/id'; +import moment from "moment"; import { useEffect, useState } from "react"; import { KeyboardAvoidingView, Platform, + Pressable, SafeAreaView, ScrollView, View @@ -37,11 +41,12 @@ export default function CreateProjectAddTask() { }) const [title, setTitle] = useState(''); const taskCreate = useSelector((state: any) => state.taskCreate) + const [dsbButton, setDsbButton] = useState(true) + const [dataDetail, setDataDetail] = useState([]) + const [modalDetail, setModalDetail] = useState(false) - const from = range.startDate - ? dayjs(range.startDate).format("DD-MM-YYYY") - : ""; - const to = range.endDate ? dayjs(range.endDate).format("DD-MM-YYYY") : ""; + const from = formatDateOnly(range.startDate, "DD-MM-YYYY"); + const to = formatDateOnly(range.endDate, "DD-MM-YYYY"); function checkAll() { if (from == "" || to == "" || title == "" || title == "null" || error.startDate || error.endDate || error.title) { @@ -62,18 +67,45 @@ export default function CreateProjectAddTask() { } } + function checkButton() { + if (range.startDate == null || range.endDate == null || range.startDate == undefined || range.endDate == undefined) { + setDsbButton(true) + setDataDetail([]) + } else { + setDsbButton(false) + const awal = formatDateOnly(range.startDate, "YYYY-MM-DD") + const akhir = formatDateOnly(range.endDate, "YYYY-MM-DD") + const datanya = getDatesInRange(awal, akhir) + setDataDetail(datanya.map((item: any) => ({ + date: item, + timeStart: null, + timeEnd: null, + }))) + } + } + useEffect(() => { checkAll() }, [from, to, title, error]) + useEffect(() => { + checkButton() + }, [range]) + async function handleCreate() { try { + const dataDetailFix = dataDetail.map((item: any) => ({ + date: moment(item.date, "DD-MM-YYYY").format("YYYY-MM-DD"), + timeStart: item.timeStart, + timeEnd: item.timeEnd, + })) 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"), + dateStartFix: formatDateOnly(range.startDate, "YYYY-MM-DD"), + dateEndFix: formatDateOnly(range.endDate, "YYYY-MM-DD"), + dataDetail: dataDetailFix }])) router.back(); } catch (error) { @@ -151,6 +183,13 @@ export default function CreateProjectAddTask() { { (error.endDate || error.startDate) && Tanggal tidak boleh kosong } + { setModalDetail(true) }} + > + Detail + + { + setDataDetail(data) + }} + /> ); } diff --git a/app/(application)/project/update/[detail].tsx b/app/(application)/project/update/[detail].tsx index 10bf637..a336e31 100644 --- a/app/(application)/project/update/[detail].tsx +++ b/app/(application)/project/update/[detail].tsx @@ -1,18 +1,21 @@ import ButtonBackHeader from "@/components/buttonBackHeader"; import ButtonSaveHeader from "@/components/buttonSaveHeader"; import { InputForm } from "@/components/inputForm"; +import ModalAddDetailTugasProject from "@/components/project/modalAddDetailTugasProject"; import Text from "@/components/Text"; import Styles from "@/constants/Styles"; import { apiEditProjectTask, apiGetProjectTask } from "@/lib/api"; +import { formatDateOnly } from "@/lib/fun_formatDateOnly"; +import { getDatesInRange } from "@/lib/fun_getDatesInRange"; import { setUpdateProject } from "@/lib/projectUpdate"; import { useAuthSession } from "@/providers/AuthProvider"; import { useHeaderHeight } from '@react-navigation/elements'; -import dayjs from "dayjs"; import { router, Stack, useLocalSearchParams } from "expo-router"; import 'intl'; import 'intl/locale-data/jsonp/id'; +import moment from "moment"; import { useEffect, useState } from "react"; -import { KeyboardAvoidingView, Platform, SafeAreaView, ScrollView, View } from "react-native"; +import { KeyboardAvoidingView, Platform, Pressable, SafeAreaView, ScrollView, View } from "react-native"; import Toast from "react-native-toast-message"; import DateTimePicker, { DateType } from "react-native-ui-datepicker"; import { useDispatch, useSelector } from "react-redux"; @@ -29,6 +32,9 @@ export default function UpdateProjectTask() { const [loading, setLoading] = useState(true) const [disableBtn, setDisableBtn] = useState(false) const [loadingSubmit, setLoadingSubmit] = useState(false) + const [dataDetail, setDataDetail] = useState([]) + const [modalDetail, setModalDetail] = useState(false) + const [dsbButton, setDsbButton] = useState(true) const [title, setTitle] = useState('') const [error, setError] = useState({ @@ -37,10 +43,8 @@ export default function UpdateProjectTask() { title: false, }) - const from = range.startDate - ? dayjs(range.startDate).format("DD-MM-YYYY") - : ''; - const to = range.endDate ? dayjs(range.endDate).format("DD-MM-YYYY") : ''; + const from = formatDateOnly(range.startDate); + const to = formatDateOnly(range.endDate); async function handleLoad() { try { @@ -57,6 +61,23 @@ export default function UpdateProjectTask() { }) setMonth(new Date(response.data.dateStart).getMonth()) setYear(new Date(response.data.dateStart).getFullYear()) + + const response2 = await apiGetProjectTask({ + user: hasil, + id: detail, + cat: "detailTask" + }); + if (response2.data.length == 0) { + const datanya = getDatesInRange(response.data.dateStart, response.data.dateEnd) + setDataDetail(datanya.map((item: any) => ({ + date: item, + timeStart: null, + timeEnd: null, + }))) + } else { + setDataDetail(response2.data) + } + } catch (error) { console.error(error); } finally { @@ -71,8 +92,22 @@ export default function UpdateProjectTask() { async function handleEdit() { try { setLoadingSubmit(true) + const dataDetailFix = dataDetail.map((item: any) => ({ + date: moment(item.date, "DD-MM-YYYY").format("YYYY-MM-DD"), + timeStart: item.timeStart, + timeEnd: item.timeEnd, + })) const hasil = await decryptToken(String(token?.current)); - const response = await apiEditProjectTask({ data: { title, dateStart: dayjs(range.startDate).format("YYYY-MM-DD"), dateEnd: dayjs(range.endDate).format("YYYY-MM-DD"), user: hasil }, id: detail }); + const response = await apiEditProjectTask({ + data: { + title, + dateStart: formatDateOnly(range.startDate, "YYYY-MM-DD"), + dateEnd: formatDateOnly(range.endDate, "YYYY-MM-DD"), + user: hasil, + dataDetail: dataDetailFix + }, + id: detail + }); if (response.success) { dispatch(setUpdateProject({ ...update, task: !update.task, progress: !update.progress })) Toast.show({ type: 'small', text1: 'Berhasil mengubah data', }) @@ -107,10 +142,32 @@ export default function UpdateProjectTask() { } } + + function checkButton() { + if (range.startDate == null || range.endDate == null || range.startDate == undefined || range.endDate == undefined) { + setDsbButton(true) + setDataDetail([]) + } else { + setDsbButton(false) + const awal = formatDateOnly(range.startDate, "YYYY-MM-DD") + const akhir = formatDateOnly(range.endDate, "YYYY-MM-DD") + const datanya = getDatesInRange(awal, akhir) + setDataDetail(datanya.map((item: any) => ({ + date: item, + timeStart: null, + timeEnd: null, + }))) + } + } + useEffect(() => { checkAll() }, [from, to, title, error]) + useEffect(() => { + checkButton() + }, [range]) + return ( Tanggal tidak boleh kosong } + { setModalDetail(true) }} + > + Detail + + + { + setDataDetail(data) + }} + /> ) } \ No newline at end of file diff --git a/components/modalFloat.tsx b/components/modalFloat.tsx index 036d2ed..dd6b855 100644 --- a/components/modalFloat.tsx +++ b/components/modalFloat.tsx @@ -33,10 +33,10 @@ export default function ModalFloat({ isVisible, setVisible, title, children, onS !buttonHide && ( { setVisible(false) }}> - Batal + Batal - Simpan + Simpan ) diff --git a/components/project/modalAddDetailTugasProject.tsx b/components/project/modalAddDetailTugasProject.tsx new file mode 100644 index 0000000..30cf6c2 --- /dev/null +++ b/components/project/modalAddDetailTugasProject.tsx @@ -0,0 +1,135 @@ +import Styles from "@/constants/Styles"; +import { stringToDateTime } from "@/lib/fun_stringToDate"; +import { useEffect, useState } from "react"; +import { Dimensions, View, VirtualizedList } from "react-native"; +import { InputDate } from "../inputDate"; +import ModalFloat from "../modalFloat"; +import Text from "../Text"; + +interface Props { + date: string; + timeStart: string; + timeEnd: string; +} + +export default function ModalAddDetailTugasProject({ isVisible, setVisible, dataTanggal, onSubmit }: { isVisible: boolean, setVisible: (value: boolean) => void, dataTanggal: Props[], onSubmit: (data: Props[]) => void }) { + const [data, setData] = useState(dataTanggal) + const tinggiScreen = Dimensions.get("window").height; + const tinggiFix = tinggiScreen * 70 / 100; + const [error, setError] = useState([]) + + useEffect(() => { + if (isVisible) { + setData(dataTanggal) + setError([]) + } + }, [isVisible, dataTanggal]) + + + const getItem = (_data: unknown, index: number): Props => ({ + date: data[index].date, + timeStart: data[index].timeStart, + timeEnd: data[index].timeEnd, + }) + + function settingError(date: string, cat: 'timeStart' | 'timeEnd', val: boolean) { + const ada = error.find((item: any) => item.date == date) + if (ada) { + setError(error.map((item: any) => { + if (item.date == date) { + return { ...item, [cat]: val } + } + return item + })) + } else { + setError([...error, { date, [cat]: val }]) + } + } + + function validationForm(cat: "timeStart" | "timeEnd", val: string, date: string) { + if (cat == "timeEnd") { + const start = stringToDateTime("", String(data.find((item) => item.date == date)?.timeStart)) + const end = stringToDateTime("", val) + const timestampAwal = start.getTime() + const timestampAkhir = end.getTime() + if (val == "" || val == null || timestampAwal > timestampAkhir) { + settingError(date, "timeEnd", true) + } else { + settingError(date, "timeEnd", false) + } + } else { + const end = stringToDateTime("", String(data.find((item) => item.date == date)?.timeEnd)) + const start = stringToDateTime("", val) + const timestampAwal = start.getTime() + const timestampAkhir = end.getTime() + + if (val == "" || val == null || timestampAwal > timestampAkhir) { + settingError(date, "timeEnd", true) + } else { + settingError(date, "timeEnd", false) + } + } + + setData(data.map((item) => { + if (item.date == date) { + return { ...item, [cat]: val } + } + return item + })) + } + + return ( + val.timeEnd == true || val.timeStart == true)} + onSubmit={() => { + onSubmit(data) + setVisible(false) + }} + > + + data.length} + getItem={getItem} + renderItem={({ item, index }: { item: Props, index: number }) => { + return ( + + {item.date} + + + { validationForm("timeStart", val, item.date) }} + value={item.timeStart} + label="Waktu Awal" + placeholder="--:--" + error={error.find((error: any) => error.date == item.date)?.timeStart} + errorText="Waktu awal tidak valid" + /> + + + { validationForm("timeEnd", val, item.date) }} + mode="time" + value={item.timeEnd} + label="Waktu Akhir" + placeholder="--:--" + error={error.find((error: any) => error.date == item.date)?.timeEnd} + errorText="Waktu akhir harus lebih dari waktu awal" + /> + + + + ) + }} + keyExtractor={(item, index) => String(index)} + showsVerticalScrollIndicator={false} + /> + + + + ) +} \ No newline at end of file diff --git a/components/project/modalListDetailTugasProject.tsx b/components/project/modalListDetailTugasProject.tsx index 8c665c5..26d01c9 100644 --- a/components/project/modalListDetailTugasProject.tsx +++ b/components/project/modalListDetailTugasProject.tsx @@ -16,7 +16,7 @@ interface Props { } export default function ModalListDetailTugasProject({ isVisible, setVisible, idTask }: { isVisible: boolean, setVisible: (value: boolean) => void, idTask: string }) { - const [data, setData] = useState([]) + const [data, setData] = useState([]) const [loading, setLoading] = useState(false) const { token, decryptToken } = useAuthSession() const arrSkeleton = Array.from({ length: 3 }, (_, index) => index) diff --git a/constants/Styles.ts b/constants/Styles.ts index 88b95a3..e89307a 100644 --- a/constants/Styles.ts +++ b/constants/Styles.ts @@ -275,6 +275,9 @@ const Styles = StyleSheet.create({ paddingVertical: 5, marginVertical: 5 }, + btnDisabled: { + backgroundColor: '#d6d8f6', + }, btnMenuRow: { width: '33%', alignItems: 'center' diff --git a/lib/api.ts b/lib/api.ts index 3a485df..b0f7637 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -281,7 +281,7 @@ export const apiReportProject = async (data: { report: string, user: string }, i return response.data; }; -export const apiCreateProjectTask = async ({ data, id }: { data: { name: string, dateStart: string, user: string, dateEnd: string }, id: string }) => { +export const apiCreateProjectTask = async ({ data, id }: { data: { name: string, dateStart: string, user: string, dateEnd: string, dataDetail: any[] }, id: string }) => { const response = await api.post(`/mobile/project/${id}`, data) return response.data; }; @@ -306,7 +306,7 @@ export const apiGetProjectTask = async ({ user, id, cat }: { user: string, id: s return response.data; }; -export const apiEditProjectTask = async ({ data, id }: { data: { title: string, dateStart: string, user: string, dateEnd: string }, id: string }) => { +export const apiEditProjectTask = async ({ data, id }: { data: { title: string, dateStart: string, user: string, dateEnd: string, dataDetail: any[] }, id: string }) => { const response = await api.post(`/mobile/project/detail/${id}`, data) return response.data; }; diff --git a/lib/fun_formatDateOnly.ts b/lib/fun_formatDateOnly.ts new file mode 100644 index 0000000..c509e0b --- /dev/null +++ b/lib/fun_formatDateOnly.ts @@ -0,0 +1,9 @@ +import dayjs from 'dayjs'; +import { DateType } from "react-native-ui-datepicker"; + +export function formatDateOnly(date?: DateType, format?: "DD-MM-YYYY" | "YYYY-MM-DD") { + if (!date) return ""; + const dateObj = dayjs.isDayjs(date) ? date.toDate() : date; + const iso = new Date(dateObj).toISOString().split("T")[0]; + return dayjs(iso).format(format || "DD-MM-YYYY"); +} \ No newline at end of file diff --git a/lib/fun_getDatesInRange.ts b/lib/fun_getDatesInRange.ts new file mode 100644 index 0000000..2ab0c67 --- /dev/null +++ b/lib/fun_getDatesInRange.ts @@ -0,0 +1,14 @@ +import dayjs from "dayjs"; + +export function getDatesInRange(startDate: string, endDate: string) { + const dates = []; + const currentDate = new Date(startDate); + const endDateNew = new Date(endDate); + + while (currentDate <= endDateNew) { + dates.push(dayjs(new Date(currentDate)).format("DD-MM-YYYY")); + currentDate.setDate(currentDate.getDate() + 1); + } + + return dates; +} \ No newline at end of file From e2c8f1db394da1de8121f198195d5eae7aa63654 Mon Sep 17 00:00:00 2001 From: amaliadwiy Date: Wed, 20 Aug 2025 17:09:15 +0800 Subject: [PATCH 2/2] upd: fitur baru task divisi Deskripsi; - tampilan list detail tugas task divisi - tampilan tambah detail tugas task divisi - tampilan edit detail tugas task divisi - tampilan tambah data task divisi > detail tugas - integrasi api get data list detail tugas task divisi - integrasi api tambah dtail tugas task divisi - integrasi api edit detail tugas task divisi - integrasi api tambah data task divisi > detail tugas NO Issues' --- .../task/[detail]/add-task.tsx | 68 +++++++-- .../(fitur-division)/task/create/task.tsx | 63 ++++++-- .../(fitur-division)/task/update/[detail].tsx | 86 ++++++++++- components/task/modalAddDetailTugasTask.tsx | 135 ++++++++++++++++++ components/task/modalListDetailTugasTask.tsx | 116 +++++++++++++++ components/task/sectionTanggalTugasTask.tsx | 29 +++- lib/api.ts | 8 +- 7 files changed, 473 insertions(+), 32 deletions(-) create mode 100644 components/task/modalAddDetailTugasTask.tsx create mode 100644 components/task/modalListDetailTugasTask.tsx diff --git a/app/(application)/division/[id]/(fitur-division)/task/[detail]/add-task.tsx b/app/(application)/division/[id]/(fitur-division)/task/[detail]/add-task.tsx index 137cd37..2d2037a 100644 --- a/app/(application)/division/[id]/(fitur-division)/task/[detail]/add-task.tsx +++ b/app/(application)/division/[id]/(fitur-division)/task/[detail]/add-task.tsx @@ -1,25 +1,28 @@ import ButtonBackHeader from "@/components/buttonBackHeader"; import ButtonSaveHeader from "@/components/buttonSaveHeader"; import { InputForm } from "@/components/inputForm"; +import ModalAddDetailTugasTask from "@/components/task/modalAddDetailTugasTask"; import Text from "@/components/Text"; import Styles from "@/constants/Styles"; import { apiCreateTaskTugas } from "@/lib/api"; +import { formatDateOnly } from "@/lib/fun_formatDateOnly"; +import { getDatesInRange } from "@/lib/fun_getDatesInRange"; import { setUpdateTask } from "@/lib/taskUpdate"; import { useAuthSession } from "@/providers/AuthProvider"; -import dayjs from "dayjs"; +import { useHeaderHeight } from '@react-navigation/elements'; import { router, Stack, useLocalSearchParams } from "expo-router"; import 'intl'; import 'intl/locale-data/jsonp/id'; +import moment from "moment"; import { useEffect, useState } from "react"; import { - KeyboardAvoidingView, Platform, SafeAreaView, + KeyboardAvoidingView, Platform, Pressable, SafeAreaView, ScrollView, View } from "react-native"; import Toast from "react-native-toast-message"; import DateTimePicker, { DateType } from "react-native-ui-datepicker"; import { useDispatch, useSelector } from "react-redux"; -import { useHeaderHeight } from '@react-navigation/elements'; export default function TaskDivisionAddTask() { const { token, decryptToken } = useAuthSession(); @@ -38,12 +41,13 @@ export default function TaskDivisionAddTask() { endDate: false, title: false, }); - const [title, setTitle] = useState(""); + const [title, setTitle] = useState("") + const [dataDetail, setDataDetail] = useState([]) + const [modalDetail, setModalDetail] = useState(false) + const [dsbButton, setDsbButton] = useState(true) - const from = range.startDate - ? dayjs(range.startDate).format("DD-MM-YYYY") - : ""; - const to = range.endDate ? dayjs(range.endDate).format("DD-MM-YYYY") : ""; + const from = formatDateOnly(range.startDate); + const to = formatDateOnly(range.endDate); function checkAll() { if ( @@ -72,21 +76,49 @@ export default function TaskDivisionAddTask() { } } + + function checkButton() { + if (range.startDate == null || range.endDate == null || range.startDate == undefined || range.endDate == undefined) { + setDsbButton(true) + setDataDetail([]) + } else { + setDsbButton(false) + const awal = formatDateOnly(range.startDate, "YYYY-MM-DD") + const akhir = formatDateOnly(range.endDate, "YYYY-MM-DD") + const datanya = getDatesInRange(awal, akhir) + setDataDetail(datanya.map((item: any) => ({ + date: item, + timeStart: null, + timeEnd: null, + }))) + } + } + useEffect(() => { checkAll(); }, [from, to, title, error]); + useEffect(() => { + checkButton() + }, [range]) + async function handleCreate() { try { setLoading(true) + const dataDetailFix = dataDetail.map((item: any) => ({ + date: moment(item.date, "DD-MM-YYYY").format("YYYY-MM-DD"), + timeStart: item.timeStart, + timeEnd: item.timeEnd, + })) const hasil = await decryptToken(String(token?.current)); const response = await apiCreateTaskTugas({ data: { title, - dateStart: dayjs(range.startDate).format("YYYY-MM-DD"), - dateEnd: dayjs(range.endDate).format("YYYY-MM-DD"), + dateStart: formatDateOnly(range.startDate, "YYYY-MM-DD"), + dateEnd: formatDateOnly(range.endDate, "YYYY-MM-DD"), user: hasil, idDivision: id, + dataDetail: dataDetailFix, }, id: detail, }); @@ -177,6 +209,13 @@ export default function TaskDivisionAddTask() { { (error.endDate || error.startDate) && Tanggal tidak boleh kosong } + { setModalDetail(true) }} + > + Detail + - + { + setDataDetail(data) + }} + /> ); } diff --git a/app/(application)/division/[id]/(fitur-division)/task/create/task.tsx b/app/(application)/division/[id]/(fitur-division)/task/create/task.tsx index cf3c03d..98ad134 100644 --- a/app/(application)/division/[id]/(fitur-division)/task/create/task.tsx +++ b/app/(application)/division/[id]/(fitur-division)/task/create/task.tsx @@ -1,17 +1,22 @@ import ButtonBackHeader from "@/components/buttonBackHeader"; import ButtonSaveHeader from "@/components/buttonSaveHeader"; import { InputForm } from "@/components/inputForm"; +import ModalAddDetailTugasTask from "@/components/task/modalAddDetailTugasTask"; import Text from "@/components/Text"; import Styles from "@/constants/Styles"; +import { formatDateOnly } from "@/lib/fun_formatDateOnly"; +import { getDatesInRange } from "@/lib/fun_getDatesInRange"; import { setTaskCreate } from "@/lib/taskCreate"; -import dayjs from "dayjs"; +import { useHeaderHeight } from '@react-navigation/elements'; import { router, Stack } from "expo-router"; import 'intl'; import 'intl/locale-data/jsonp/id'; +import moment from "moment"; import { useEffect, useState } from "react"; import { KeyboardAvoidingView, Platform, + Pressable, SafeAreaView, ScrollView, View @@ -20,7 +25,6 @@ import DateTimePicker, { DateType } from "react-native-ui-datepicker"; import { useDispatch, useSelector } from "react-redux"; -import { useHeaderHeight } from '@react-navigation/elements'; export default function CreateTaskAddTugas() { const headerHeight = useHeaderHeight(); @@ -37,11 +41,12 @@ export default function CreateTaskAddTugas() { }) const [title, setTitle] = useState(''); const taskCreate = useSelector((state: any) => state.taskCreate) + const [dsbButton, setDsbButton] = useState(true) + const [dataDetail, setDataDetail] = useState([]) + const [modalDetail, setModalDetail] = useState(false) - const from = range.startDate - ? dayjs(range.startDate).format("DD-MM-YYYY") - : ""; - const to = range.endDate ? dayjs(range.endDate).format("DD-MM-YYYY") : ""; + const from = formatDateOnly(range.startDate, "DD-MM-YYYY") + const to = formatDateOnly(range.endDate, "DD-MM-YYYY") function checkAll() { if (from == "" || to == "" || title == "" || title == "null" || error.startDate || error.endDate || error.title) { @@ -62,18 +67,45 @@ export default function CreateTaskAddTugas() { } } + function checkButton() { + if (range.startDate == null || range.endDate == null || range.startDate == undefined || range.endDate == undefined) { + setDsbButton(true) + setDataDetail([]) + } else { + setDsbButton(false) + const awal = formatDateOnly(range.startDate, "YYYY-MM-DD") + const akhir = formatDateOnly(range.endDate, "YYYY-MM-DD") + const datanya = getDatesInRange(awal, akhir) + setDataDetail(datanya.map((item: any) => ({ + date: item, + timeStart: null, + timeEnd: null, + }))) + } + } + useEffect(() => { checkAll() }, [from, to, title, error]) + useEffect(() => { + checkButton() + }, [range]) + async function handleCreate() { try { + const dataDetailFix = dataDetail.map((item: any) => ({ + date: moment(item.date, "DD-MM-YYYY").format("YYYY-MM-DD"), + timeStart: item.timeStart, + timeEnd: item.timeEnd, + })) 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"), + dateStartFix: formatDateOnly(range.startDate, "YYYY-MM-DD"), + dateEndFix: formatDateOnly(range.endDate, "YYYY-MM-DD"), + dataDetail: dataDetailFix }])) router.back(); } catch (error) { @@ -151,6 +183,13 @@ export default function CreateTaskAddTugas() { { (error.endDate || error.startDate) && Tanggal tidak boleh kosong } + { setModalDetail(true) }} + > + Detail + + { + setDataDetail(data) + }} + /> ); } diff --git a/app/(application)/division/[id]/(fitur-division)/task/update/[detail].tsx b/app/(application)/division/[id]/(fitur-division)/task/update/[detail].tsx index 5f48688..552ab27 100644 --- a/app/(application)/division/[id]/(fitur-division)/task/update/[detail].tsx +++ b/app/(application)/division/[id]/(fitur-division)/task/update/[detail].tsx @@ -1,20 +1,24 @@ import ButtonBackHeader from "@/components/buttonBackHeader"; import ButtonSaveHeader from "@/components/buttonSaveHeader"; import { InputForm } from "@/components/inputForm"; +import ModalAddDetailTugasTask from "@/components/task/modalAddDetailTugasTask"; import Text from "@/components/Text"; import Styles from "@/constants/Styles"; import { apiEditTaskTugas, apiGetTaskTugas } from "@/lib/api"; +import { formatDateOnly } from "@/lib/fun_formatDateOnly"; +import { getDatesInRange } from "@/lib/fun_getDatesInRange"; import { setUpdateTask } from "@/lib/taskUpdate"; import { useAuthSession } from "@/providers/AuthProvider"; import { useHeaderHeight } from '@react-navigation/elements'; -import dayjs from "dayjs"; import { router, Stack, useLocalSearchParams } from "expo-router"; import 'intl'; import 'intl/locale-data/jsonp/id'; +import moment from "moment"; import { useEffect, useState } from "react"; import { KeyboardAvoidingView, Platform, + Pressable, SafeAreaView, ScrollView, View @@ -38,6 +42,9 @@ export default function UpdateProjectTaskDivision() { const [year, setYear] = useState(); const [loading, setLoading] = useState(true); const [disableBtn, setDisableBtn] = useState(false); + const [dataDetail, setDataDetail] = useState([]) + const [modalDetail, setModalDetail] = useState(false) + const [dsbButton, setDsbButton] = useState(true) const [title, setTitle] = useState(""); const [error, setError] = useState({ startDate: false, @@ -45,10 +52,8 @@ export default function UpdateProjectTaskDivision() { title: false, }); - const from = range.startDate - ? dayjs(range.startDate).format("DD-MM-YYYY") - : ""; - const to = range.endDate ? dayjs(range.endDate).format("DD-MM-YYYY") : ""; + const from = formatDateOnly(range.startDate); + const to = formatDateOnly(range.endDate); async function handleLoad() { try { @@ -65,6 +70,22 @@ export default function UpdateProjectTaskDivision() { }); setMonth(new Date(response.data.dateStart).getMonth()); setYear(new Date(response.data.dateStart).getFullYear()); + + const response2 = await apiGetTaskTugas({ + user: hasil, + id: detail, + cat: "detailTask" + }); + if (response2.data.length == 0) { + const datanya = getDatesInRange(response.data.dateStart, response.data.dateEnd) + setDataDetail(datanya.map((item: any) => ({ + date: item, + timeStart: null, + timeEnd: null, + }))) + } else { + setDataDetail(response2.data) + } } catch (error) { console.error(error); } finally { @@ -79,8 +100,22 @@ export default function UpdateProjectTaskDivision() { async function handleEdit() { try { setLoadingSubmit(true) + const dataDetailFix = dataDetail.map((item: any) => ({ + date: moment(item.date, "DD-MM-YYYY").format("YYYY-MM-DD"), + timeStart: item.timeStart, + timeEnd: item.timeEnd, + })) const hasil = await decryptToken(String(token?.current)); - const response = await apiEditTaskTugas({ data: { title, dateStart: dayjs(range.startDate).format("YYYY-MM-DD"), dateEnd: dayjs(range.endDate).format("YYYY-MM-DD"), user: hasil }, id: detail }); + const response = await apiEditTaskTugas({ + data: { + title, + dateStart: formatDateOnly(range.startDate, "YYYY-MM-DD"), + dateEnd: formatDateOnly(range.endDate, "YYYY-MM-DD"), + user: hasil, + dataDetail: dataDetailFix + }, + id: detail + }); if (response.success) { dispatch(setUpdateTask({ ...update, task: !update.task })) Toast.show({ type: 'small', text1: 'Berhasil mengubah data', }) @@ -123,10 +158,33 @@ export default function UpdateProjectTaskDivision() { } } + + function checkButton() { + if (range.startDate == null || range.endDate == null || range.startDate == undefined || range.endDate == undefined) { + setDsbButton(true) + setDataDetail([]) + } else { + setDsbButton(false) + const awal = formatDateOnly(range.startDate, "YYYY-MM-DD") + const akhir = formatDateOnly(range.endDate, "YYYY-MM-DD") + const datanya = getDatesInRange(awal, akhir) + setDataDetail(datanya.map((item: any) => ({ + date: item, + timeStart: null, + timeEnd: null, + }))) + } + } + useEffect(() => { checkAll(); }, [from, to, title, error]); + + useEffect(() => { + checkButton() + }, [range]) + return ( )} + { setModalDetail(true) }} + > + Detail + - + { + setDataDetail(data) + }} + /> ); } diff --git a/components/task/modalAddDetailTugasTask.tsx b/components/task/modalAddDetailTugasTask.tsx new file mode 100644 index 0000000..ca98600 --- /dev/null +++ b/components/task/modalAddDetailTugasTask.tsx @@ -0,0 +1,135 @@ +import Styles from "@/constants/Styles"; +import { stringToDateTime } from "@/lib/fun_stringToDate"; +import { useEffect, useState } from "react"; +import { Dimensions, View, VirtualizedList } from "react-native"; +import { InputDate } from "../inputDate"; +import ModalFloat from "../modalFloat"; +import Text from "../Text"; + +interface Props { + date: string; + timeStart: string; + timeEnd: string; +} + +export default function ModalAddDetailTugasTask({ isVisible, setVisible, dataTanggal, onSubmit }: { isVisible: boolean, setVisible: (value: boolean) => void, dataTanggal: Props[], onSubmit: (data: Props[]) => void }) { + const [data, setData] = useState(dataTanggal) + const tinggiScreen = Dimensions.get("window").height; + const tinggiFix = tinggiScreen * 70 / 100; + const [error, setError] = useState([]) + + useEffect(() => { + if (isVisible) { + setData(dataTanggal) + setError([]) + } + }, [isVisible, dataTanggal]) + + + const getItem = (_data: unknown, index: number): Props => ({ + date: data[index].date, + timeStart: data[index].timeStart, + timeEnd: data[index].timeEnd, + }) + + function settingError(date: string, cat: 'timeStart' | 'timeEnd', val: boolean) { + const ada = error.find((item: any) => item.date == date) + if (ada) { + setError(error.map((item: any) => { + if (item.date == date) { + return { ...item, [cat]: val } + } + return item + })) + } else { + setError([...error, { date, [cat]: val }]) + } + } + + function validationForm(cat: "timeStart" | "timeEnd", val: string, date: string) { + if (cat == "timeEnd") { + const start = stringToDateTime("", String(data.find((item) => item.date == date)?.timeStart)) + const end = stringToDateTime("", val) + const timestampAwal = start.getTime() + const timestampAkhir = end.getTime() + if (val == "" || val == null || timestampAwal > timestampAkhir) { + settingError(date, "timeEnd", true) + } else { + settingError(date, "timeEnd", false) + } + } else { + const end = stringToDateTime("", String(data.find((item) => item.date == date)?.timeEnd)) + const start = stringToDateTime("", val) + const timestampAwal = start.getTime() + const timestampAkhir = end.getTime() + + if (val == "" || val == null || timestampAwal > timestampAkhir) { + settingError(date, "timeEnd", true) + } else { + settingError(date, "timeEnd", false) + } + } + + setData(data.map((item) => { + if (item.date == date) { + return { ...item, [cat]: val } + } + return item + })) + } + + return ( + val.timeEnd == true || val.timeStart == true)} + onSubmit={() => { + onSubmit(data) + setVisible(false) + }} + > + + data.length} + getItem={getItem} + renderItem={({ item, index }: { item: Props, index: number }) => { + return ( + + {item.date} + + + { validationForm("timeStart", val, item.date) }} + value={item.timeStart} + label="Waktu Awal" + placeholder="--:--" + error={error.find((error: any) => error.date == item.date)?.timeStart} + errorText="Waktu awal tidak valid" + /> + + + { validationForm("timeEnd", val, item.date) }} + mode="time" + value={item.timeEnd} + label="Waktu Akhir" + placeholder="--:--" + error={error.find((error: any) => error.date == item.date)?.timeEnd} + errorText="Waktu akhir harus lebih dari waktu awal" + /> + + + + ) + }} + keyExtractor={(item, index) => String(index)} + showsVerticalScrollIndicator={false} + /> + + + + ) +} \ No newline at end of file diff --git a/components/task/modalListDetailTugasTask.tsx b/components/task/modalListDetailTugasTask.tsx new file mode 100644 index 0000000..b909ac2 --- /dev/null +++ b/components/task/modalListDetailTugasTask.tsx @@ -0,0 +1,116 @@ +import Styles from "@/constants/Styles"; +import { apiGetProjectTask, apiGetTaskTugas } from "@/lib/api"; +import { useAuthSession } from "@/providers/AuthProvider"; +import { useEffect, useState } from "react"; +import { Dimensions, View, VirtualizedList } from "react-native"; +import { InputDate } from "../inputDate"; +import ModalFloat from "../modalFloat"; +import Skeleton from "../skeleton"; +import Text from "../Text"; + +interface Props { + id: string; + date: string; + timeStart: string; + timeEnd: string; +} + +export default function ModalListDetailTugasTask({ isVisible, setVisible, idTask }: { isVisible: boolean, setVisible: (value: boolean) => void, idTask: string }) { + const [data, setData] = useState([]) + const [loading, setLoading] = useState(false) + const { token, decryptToken } = useAuthSession() + const arrSkeleton = Array.from({ length: 3 }, (_, index) => index) + const tinggiScreen = Dimensions.get("window").height; + const tinggiFix = tinggiScreen * 70 / 100; + + + + async function getData() { + try { + setLoading(true) + const hasil = await decryptToken(String(token?.current)); + const res = await apiGetTaskTugas({ user: hasil, id: idTask, cat: "detailTask" }) + setData(res.data) + } catch (error) { + console.error(error) + } finally { + setLoading(false) + } + } + + useEffect(() => { + if (isVisible) { + getData() + } + }, [isVisible, idTask]) + + const getItem = (_data: unknown, index: number): Props => ({ + id: data[index].id, + date: data[index].date, + timeStart: data[index].timeStart, + timeEnd: data[index].timeEnd, + }) + + return ( + + + { + loading ? + arrSkeleton.map((item: any, i: number) => { + return ( + + ) + }) + : + data.length > 0 ? + ( + data.length} + getItem={getItem} + renderItem={({ item, index }: { item: Props, index: number }) => { + return ( + + {item.date} + + + { }} + value={item.timeStart} + label="Waktu Awal" + placeholder="--:--" + /> + + + { }} + mode="time" + value={item.timeEnd} + label="Waktu Akhir" + placeholder="--:--" + disable + /> + + + + ) + }} + keyExtractor={(item, index) => String(index)} + showsVerticalScrollIndicator={false} + /> + ) + : + Tidak ada data + } + + + + ) +} \ No newline at end of file diff --git a/components/task/sectionTanggalTugasTask.tsx b/components/task/sectionTanggalTugasTask.tsx index 2501c64..69977d2 100644 --- a/components/task/sectionTanggalTugasTask.tsx +++ b/components/task/sectionTanggalTugasTask.tsx @@ -15,6 +15,7 @@ import MenuItemRow from "../menuItemRow"; import ModalSelect from "../modalSelect"; import SkeletonTask from "../skeletonTask"; import Text from "../Text"; +import ModalListDetailTugasTask from "./modalListDetailTugasTask"; type Props = { @@ -25,7 +26,7 @@ type Props = { dateEnd: string; } -export default function SectionTanggalTugasTask({refreshing}: {refreshing: boolean}) { +export default function SectionTanggalTugasTask({ refreshing }: { refreshing: boolean }) { const dispatch = useDispatch() const update = useSelector((state: any) => state.taskUpdate) const [isModal, setModal] = useState(false) @@ -33,6 +34,7 @@ export default function SectionTanggalTugasTask({refreshing}: {refreshing: boole const { token, decryptToken } = useAuthSession() const [loading, setLoading] = useState(true) const arrSkeleton = Array.from({ length: 5 }) + const [modalDetail, setModalDetail] = useState(false) const { id, detail } = useLocalSearchParams<{ id: string, detail: string }>(); const [data, setData] = useState([]) const [tugas, setTugas] = useState({ @@ -171,7 +173,24 @@ export default function SectionTanggalTugasTask({refreshing}: {refreshing: boole router.push(`./update/${tugas.id}`) }} /> - + + } + title="Detail Waktu" + onPress={() => { + setModal(false); + setTimeout(() => { + setModalDetail(true) + }, 600) + }} + /> + + } title="Hapus Tugas" @@ -198,6 +217,12 @@ export default function SectionTanggalTugasTask({refreshing}: {refreshing: boole open={isSelect} valChoose={String(tugas.status)} /> + + ) } \ No newline at end of file diff --git a/lib/api.ts b/lib/api.ts index b0f7637..f22db38 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -529,12 +529,12 @@ export const apiUpdateStatusTaskDivision = async (data: { user: string, status: return response.data }; -export const apiGetTaskTugas = async ({ user, id }: { user: string, id: string }) => { - const response = await api.get(`mobile/task/detail/${id}?user=${user}`); +export const apiGetTaskTugas = async ({ user, id, cat }: { user: string, id: string, cat?: string }) => { + const response = await api.get(`mobile/task/detail/${id}?user=${user}${cat ? `&cat=${cat}` : ""}`); return response.data; }; -export const apiEditTaskTugas = async ({ data, id }: { data: { title: string, dateStart: string, user: string, dateEnd: string }, id: string }) => { +export const apiEditTaskTugas = async ({ data, id }: { data: { title: string, dateStart: string, user: string, dateEnd: string, dataDetail: any[] }, id: string }) => { const response = await api.post(`/mobile/task/detail/${id}`, data) return response.data; }; @@ -559,7 +559,7 @@ export const apiDeleteTaskMember = async (data: { user: string, idUser: string } return response.data }; -export const apiCreateTaskTugas = async ({ data, id }: { data: { title: string, dateStart: string, user: string, dateEnd: string, idDivision: string }, id: string }) => { +export const apiCreateTaskTugas = async ({ data, id }: { data: { title: string, dateStart: string, user: string, dateEnd: string, idDivision: string, dataDetail: any[] }, id: string }) => { const response = await api.post(`/mobile/task/${id}`, data) return response.data; };