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 029b676..f1d7118 100644
--- a/app/(application)/division/[id]/(fitur-division)/task/[detail]/index.tsx
+++ b/app/(application)/division/[id]/(fitur-division)/task/[detail]/index.tsx
@@ -154,7 +154,7 @@ export default function DetailTaskDivision() {
}
-
+
diff --git a/app/(application)/division/[id]/(fitur-division)/task/index.tsx b/app/(application)/division/[id]/(fitur-division)/task/index.tsx
index ada348b..b946d3b 100644
--- a/app/(application)/division/[id]/(fitur-division)/task/index.tsx
+++ b/app/(application)/division/[id]/(fitur-division)/task/index.tsx
@@ -21,6 +21,7 @@ import { router, useLocalSearchParams } from "expo-router";
import { useEffect, useState } from "react";
import { Pressable, RefreshControl, ScrollView, View, VirtualizedList } from "react-native";
import { useSelector } from "react-redux";
+import AsyncStorage from "@react-native-async-storage/async-storage";
type Props = {
id: string;
@@ -36,6 +37,18 @@ export default function ListTask() {
const { id, status, year } = useLocalSearchParams<{ id: string; status: string; year: string }>()
const [isList, setList] = useState(false)
const { token, decryptToken } = useAuthSession()
+
+ useEffect(() => {
+ AsyncStorage.getItem('division_view_mode').then((val) => {
+ if (val !== null) setList(val === 'list')
+ })
+ }, [])
+
+ function toggleView() {
+ const next = !isList
+ setList(next)
+ AsyncStorage.setItem('division_view_mode', next ? 'list' : 'grid')
+ }
const [data, setData] = useState([])
const [search, setSearch] = useState("")
const update = useSelector((state: any) => state.taskUpdate)
@@ -172,13 +185,9 @@ export default function ListTask() {
n={4}
/>
-
+
- {
- setList(!isList);
- }}
- >
+
-
+
+
}
title={item.title}
diff --git a/app/(application)/division/index.tsx b/app/(application)/division/index.tsx
index 40bbc82..05a6afd 100644
--- a/app/(application)/division/index.tsx
+++ b/app/(application)/division/index.tsx
@@ -17,6 +17,7 @@ import {
Ionicons,
MaterialCommunityIcons
} from "@expo/vector-icons";
+import AsyncStorage from "@react-native-async-storage/async-storage";
import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
import { router, useLocalSearchParams } from "expo-router";
import { useEffect, useMemo, useState } from "react";
@@ -37,6 +38,18 @@ export default function ListDivision() {
cat?: string;
}>();
const [isList, setList] = useState(false);
+
+ useEffect(() => {
+ AsyncStorage.getItem('division_view_mode').then((val) => {
+ if (val !== null) setList(val === 'list')
+ })
+ }, [])
+
+ function toggleView() {
+ const next = !isList
+ setList(next)
+ AsyncStorage.setItem('division_view_mode', next ? 'list' : 'grid')
+ }
const entityUser = useSelector((state: any) => state.user)
const { token, decryptToken } = useAuthSession()
const { colors } = useTheme();
@@ -184,11 +197,7 @@ export default function ListDivision() {
- {
- setList(!isList);
- }}
- >
+
router.back()}
right={
- (entityUser.role != "user") && isEdit ? : <>>
+ (entityUser.role != "user") && isEdit ? : <>>
}
/>
)
@@ -109,51 +110,71 @@ export default function MemberDetail() {
colors={[colors.header, colors.homeGradient]}
style={[Styles.wrapHeadViewMember]}
>
- {
- loading ?
- <>
-
-
-
- >
- :
- <>
- setPreview(true)}>
+ {loading ? (
+ <>
+
+
+
+ >
+ ) : (
+ <>
+ setPreview(true)}>
+
-
- {data?.name}
- {data?.role}
- >
-
- }
+
+
+ {data?.name}
+ {data?.role}
+
+ {data?.isApprover && (
+
+ APPROVER
+
+ )}
+
+ {data?.isActive ? 'AKTIF' : 'TIDAK AKTIF'}
+
+
+ >
+ )}
-
-
- Informasi
-
-
- {
- loading ?
- arrSkeleton.map((item, index) => {
- return (
-
- )
- })
- :
- <>
-
-
-
-
-
-
- >
- }
+
+ Informasi
+
+ {loading ? (
+ arrSkeleton.map((_, index) => (
+
+
+
+
+
+
+ ))
+ ) : (
+ [
+ { icon: , label: 'NIK', value: data?.nik },
+ { icon: , label: 'Lembaga Desa', value: data?.group },
+ { icon: , label: 'Jabatan', value: data?.position },
+ { icon: , label: 'No Telepon', value: `+62${data?.phone}` },
+ { icon: , label: 'Email', value: data?.email },
+ { icon: , label: 'Jenis Kelamin', value: data?.gender == "F" ? "Perempuan" : "Laki-Laki" },
+ ].map((item, index, arr) => (
+
+
+ {item.icon}
+
+
+ {item.label}
+ {item.value ?? '-'}
+
+
+ ))
+ )}
+
diff --git a/app/(application)/profile.tsx b/app/(application)/profile.tsx
index e9c90af..52c2f72 100644
--- a/app/(application)/profile.tsx
+++ b/app/(application)/profile.tsx
@@ -1,6 +1,5 @@
import AppHeader from "@/components/AppHeader";
import { ButtonHeader } from "@/components/buttonHeader";
-import ItemDetailMember from "@/components/itemDetailMember";
import Text from "@/components/Text";
import { assetUserImage } from "@/constants/AssetsError";
import { ConstEnv } from "@/constants/ConstEnv";
@@ -9,7 +8,7 @@ import { apiGetProfile } from "@/lib/api";
import { setEntities } from "@/lib/entitiesSlice";
import { useAuthSession } from "@/providers/AuthProvider";
import { useTheme } from "@/providers/ThemeProvider";
-import { Feather } from "@expo/vector-icons";
+import { Feather, MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons";
import { LinearGradient } from "expo-linear-gradient";
import { router, Stack } from "expo-router";
import { useState } from "react";
@@ -42,6 +41,15 @@ export default function Profile() {
setRefreshing(false)
};
+ const infoRows = [
+ { icon: , label: 'NIK', value: entities.nik },
+ { icon: , label: 'Lembaga Desa', value: entities.group },
+ { icon: , label: 'Jabatan', value: entities.position },
+ { icon: , label: 'No Telepon', value: `0${entities.phone}` },
+ { icon: , label: 'Email', value: entities.email },
+ { icon: , label: 'Jenis Kelamin', value: entities.gender == "F" ? 'Perempuan' : 'Laki-laki' },
+ ]
+
return (
}
- onPress={() => {
- router.push('/setting')
- }}
+ onPress={() => router.push('/setting')}
/>
}
/>
@@ -75,32 +81,47 @@ export default function Profile() {
}
style={[Styles.h100, { backgroundColor: colors.background }]}
>
-
-
- setPreview(true)}>
+
+ setPreview(true)}>
+
{ setError(true) }}
+ onError={() => setError(true)}
style={[Styles.userProfileBig]}
/>
-
- {entities.name}
- {entities.role}
-
-
-
- Informasi
- {/* Note: ItemDetailMember might need updates to support dynamic colors if it uses default text colors */}
-
-
-
-
-
-
+
+ {entities.name}
+ {entities.role}
+ {entities.isApprover && (
+
+
+ APPROVER
+
+
+ )}
+
+
+
+ Informasi
+
+ {infoRows.map((item, index, arr) => (
+
+
+ {item.icon}
+
+
+ {item.label}
+ {item.value ?? '-'}
+
+
+ ))}
@@ -114,4 +135,4 @@ export default function Profile() {
/>
)
-}
\ No newline at end of file
+}
diff --git a/app/(application)/project/index.tsx b/app/(application)/project/index.tsx
index f283d04..23b44f8 100644
--- a/app/(application)/project/index.tsx
+++ b/app/(application)/project/index.tsx
@@ -23,6 +23,7 @@ import { router, useLocalSearchParams } from "expo-router";
import { useEffect, useMemo, useState } from "react";
import { Pressable, RefreshControl, ScrollView, View, VirtualizedList } from "react-native";
import { useSelector } from "react-redux";
+import AsyncStorage from "@react-native-async-storage/async-storage";
type Props = {
id: string;
@@ -50,6 +51,18 @@ export default function ListProject() {
const [search, setSearch] = useState("")
const [isList, setList] = useState(false)
const update = useSelector((state: any) => state.projectUpdate)
+
+ useEffect(() => {
+ AsyncStorage.getItem('division_view_mode').then((val) => {
+ if (val !== null) setList(val === 'list')
+ })
+ }, [])
+
+ function toggleView() {
+ const next = !isList
+ setList(next)
+ AsyncStorage.setItem('division_view_mode', next ? 'list' : 'grid')
+ }
const queryClient = useQueryClient()
const [refreshing, setRefreshing] = useState(false)
@@ -188,11 +201,7 @@ export default function ListProject() {
- {
- setList(!isList);
- }}
- >
+
void
+ data: ApprovalRecord[]
+ loading: boolean
+}
+
+function ApprovalStatusBadge({ status }: { status: number }) {
+ const { colors } = useTheme()
+ const config =
+ status === 1
+ ? { label: 'Disetujui', color: colors.success }
+ : status === 2
+ ? { label: 'Ditolak', color: colors.error }
+ : { label: 'Menunggu', color: '#FFA94D' }
+
+ return (
+
+
+ {config.label}
+
+
+ )
+}
+
+export default function ModalRiwayatApproval({ isVisible, setVisible, data, loading }: Props) {
+ const { colors } = useTheme()
+ const arrSkeleton = Array.from({ length: 3 })
+ const scrollRef = useRef(null)
+ const [scrollOffset, setScrollOffset] = useState(0)
+
+ return (
+ scrollRef.current?.scrollTo(p)}
+ >
+ setScrollOffset(nativeEvent.contentOffset.y)}
+ scrollEventThrottle={16}
+ >
+ {loading ? (
+ arrSkeleton.map((_, i) => (
+
+
+
+ ))
+ ) : data.length > 0 ? (
+ data.map((item, index) => (
+
+ {/* Status + tanggal */}
+
+
+
+ {item.createdAt}
+
+
+
+ {/* Pengaju */}
+
+
+ Diajukan Oleh:
+ {item.submitter.name}
+
+
+ {/* Approver */}
+
+
+ Disetujui Oleh:
+
+ {item.approver?.name ?? '-'}
+
+
+
+ {/* Catatan penolakan */}
+ {item.note && (
+
+
+ Alasan Penolakan
+
+
+ {item.note}
+
+
+ )}
+
+ ))
+ ) : (
+
+ Belum ada riwayat persetujuan
+
+ )}
+
+
+ )
+}
diff --git a/components/ModalTolakApproval.tsx b/components/ModalTolakApproval.tsx
new file mode 100644
index 0000000..3f56707
--- /dev/null
+++ b/components/ModalTolakApproval.tsx
@@ -0,0 +1,78 @@
+import Styles from "@/constants/Styles"
+import { useTheme } from "@/providers/ThemeProvider"
+import { useState } from "react"
+import { TouchableOpacity, View } from "react-native"
+import DrawerBottom from "./drawerBottom"
+import { InputForm } from "./inputForm"
+import Text from "./Text"
+
+type Props = {
+ isVisible: boolean
+ setVisible: (value: boolean) => void
+ onTolak: (note: string) => void
+ loading?: boolean
+}
+
+export default function ModalTolakApproval({ isVisible, setVisible, onTolak, loading }: Props) {
+ const { colors } = useTheme()
+ const [note, setNote] = useState('')
+ const [error, setError] = useState(false)
+
+ function handleClose(value: boolean) {
+ setNote('')
+ setError(false)
+ setVisible(value)
+ }
+
+ function handleSubmit() {
+ if (!note.trim()) {
+ setError(true)
+ return
+ }
+ onTolak(note.trim())
+ setNote('')
+ setError(false)
+ }
+
+ return (
+
+
+
+
+
+
+
+ {loading ? 'Memproses...' : 'Tolak Tugas'}
+
+
+
+ )
+}
diff --git a/components/drawerBottom.tsx b/components/drawerBottom.tsx
index 9b964c9..3eb96e2 100644
--- a/components/drawerBottom.tsx
+++ b/components/drawerBottom.tsx
@@ -14,9 +14,11 @@ type Props = {
height?: number
backdropPressable?: boolean
keyboard?: boolean
+ scrollOffset?: number
+ scrollTo?: (p: any) => void
}
-export default function DrawerBottom({ isVisible, setVisible, title, children, animation, height, backdropPressable = true, keyboard = false }: Props) {
+export default function DrawerBottom({ isVisible, setVisible, title, children, animation, height, backdropPressable = true, keyboard = false, scrollOffset, scrollTo }: Props) {
const tinggiScreen = Dimensions.get("window").height;
const { colors } = useTheme();
const tinggiInput = height != undefined ? height : 25
@@ -38,6 +40,9 @@ export default function DrawerBottom({ isVisible, setVisible, title, children, a
backdropTransitionOutTiming={500}
useNativeDriverForBackdrop={true}
propagateSwipe={true}
+ scrollTo={scrollTo}
+ scrollOffset={scrollOffset}
+ scrollOffsetMax={200}
>
{
keyboard ?
diff --git a/components/home/carouselHome.tsx b/components/home/carouselHome.tsx
index d570cc1..80c5e20 100644
--- a/components/home/carouselHome.tsx
+++ b/components/home/carouselHome.tsx
@@ -36,7 +36,7 @@ export default function CaraouselHome({ refreshing }: { refreshing: boolean }) {
async function handleUser() {
const hasil = await decryptToken(String(token?.current))
const response = await apiGetProfile({ id: hasil })
- dispatch(setEntityUser({ role: response.data.idUserRole, admin: false }))
+ dispatch(setEntityUser({ role: response.data.idUserRole, admin: false, isApprover: response.data.isApprover ?? false }))
}
useEffect(() => {
diff --git a/components/home/carouselHome2.tsx b/components/home/carouselHome2.tsx
index a73ac74..501385f 100644
--- a/components/home/carouselHome2.tsx
+++ b/components/home/carouselHome2.tsx
@@ -59,7 +59,7 @@ export default function CaraouselHome2({ refreshing }: { refreshing: boolean })
// Sync User Role to Redux
useEffect(() => {
if (profile) {
- dispatch(setEntityUser({ role: profile.idUserRole, admin: false }))
+ dispatch(setEntityUser({ role: profile.idUserRole, admin: false, isApprover: profile.isApprover ?? false }))
}
}, [profile, dispatch])
diff --git a/components/itemSectionTanggalTugas.tsx b/components/itemSectionTanggalTugas.tsx
index b5763e3..423a94c 100644
--- a/components/itemSectionTanggalTugas.tsx
+++ b/components/itemSectionTanggalTugas.tsx
@@ -11,7 +11,7 @@ type FileItem = {
}
type Props = {
- done?: boolean
+ status?: number // 0=belum selesai, 1=selesai, 2=menunggu persetujuan
title: string
dateStart: string
dateEnd: string
@@ -64,7 +64,15 @@ function getFileIcon(extension: string): keyof typeof MaterialCommunityIcons.gly
return 'file-outline'
}
-export default function ItemSectionTanggalTugas({ done, title, dateStart, dateEnd, files = [], onPress }: Props) {
+const AMBER = '#FFA94D'
+
+function getStatusStyle(status: number | undefined, successColor: string, dimmed: string) {
+ if (status === 1) return { accent: successColor, badge: successColor + '25', text: successColor, label: 'Selesai' }
+ if (status === 2) return { accent: AMBER, badge: AMBER + '25', text: AMBER, label: 'Menunggu Persetujuan' }
+ return { accent: dimmed + '80', badge: dimmed + '18', text: dimmed, label: 'Belum Selesai' }
+}
+
+export default function ItemSectionTanggalTugas({ status, title, dateStart, dateEnd, files = [], onPress }: Props) {
const { colors, activeTheme } = useTheme()
const [containerWidth, setContainerWidth] = useState(0)
@@ -77,7 +85,7 @@ export default function ItemSectionTanggalTugas({ done, title, dateStart, dateEn
const dimmed = colors.dimmed.slice(0, 7)
const successColor = activeTheme === 'dark' ? '#51CF66' : colors.success
- const accentColor = done === true ? successColor : dimmed + '80'
+ const statusStyle = getStatusStyle(status, successColor, dimmed)
return (
{/* Accent bar kiri */}
- {done !== undefined && (
-
+ {status !== undefined && (
+
)}
{/* Konten */}
@@ -103,16 +111,16 @@ export default function ItemSectionTanggalTugas({ done, title, dateStart, dateEn
{/* Judul + badge status */}
{title}
- {done !== undefined && (
+ {status !== undefined && (
-
- {done ? 'Selesai' : 'Belum Selesai'}
+
+ {statusStyle.label}
)}
diff --git a/components/member/headerMemberDetail.tsx b/components/member/headerMemberDetail.tsx
index 3b8044c..7dd153b 100644
--- a/components/member/headerMemberDetail.tsx
+++ b/components/member/headerMemberDetail.tsx
@@ -1,5 +1,5 @@
import Styles from "@/constants/Styles"
-import { apiDeleteUser } from "@/lib/api"
+import { apiDeleteUser, apiToggleApprover } from "@/lib/api"
import { setUpdateMember } from "@/lib/memberSlice"
import { useAuthSession } from "@/providers/AuthProvider"
import { useTheme } from "@/providers/ThemeProvider"
@@ -16,14 +16,17 @@ import MenuItemRow from "../menuItemRow"
type Props = {
active: any,
- id: string
+ id: string,
+ isApprover: boolean,
}
-export default function HeaderRightMemberDetail({ active, id }: Props) {
+export default function HeaderRightMemberDetail({ active, id, isApprover }: Props) {
const { token, decryptToken } = useAuthSession()
const [isVisible, setVisible] = useState(false)
const update = useSelector((state: any) => state.memberUpdate)
- const [showModal, setShowModal] = useState(false)
+ const entityUser = useSelector((state: any) => state.user)
+ const [showModalActive, setShowModalActive] = useState(false)
+ const [showModalApprover, setShowModalApprover] = useState(false)
const { colors } = useTheme();
const dispatch = useDispatch()
@@ -37,17 +40,36 @@ export default function HeaderRightMemberDetail({ active, id }: Props) {
} else {
Toast.show({ type: 'small', text1: response.message, })
}
- } catch (error : any ) {
+ } catch (error: any) {
console.error(error);
const message = error?.response?.data?.message || "Gagal mengupdate data"
-
Toast.show({ type: 'small', text1: message })
} finally {
setVisible(false)
}
-
}
+ async function handleToggleApprover() {
+ try {
+ const hasil = await decryptToken(String(token?.current))
+ const response = await apiToggleApprover({ user: hasil, isApprover: !isApprover }, id)
+ if (response.success) {
+ Toast.show({ type: 'small', text1: response.message })
+ dispatch(setUpdateMember(!update))
+ } else {
+ Toast.show({ type: 'small', text1: response.message })
+ }
+ } catch (error: any) {
+ console.error(error);
+ const message = error?.response?.data?.message || "Gagal mengupdate data"
+ Toast.show({ type: 'small', text1: message })
+ } finally {
+ setShowModalApprover(false)
+ }
+ }
+
+ const canManageApprover = ['supadmin', 'developer'].includes(entityUser.role)
+
return (
<>
{ setVisible(true) }} />
@@ -55,12 +77,10 @@ export default function HeaderRightMemberDetail({ active, id }: Props) {
}
- title={active ? "Non Aktifkan" : "Aktifkan"}
+ title={active ? "Nonaktifkan" : "Aktifkan"}
onPress={() => {
setVisible(false)
- setTimeout(() => {
- setShowModal(true)
- }, 600)
+ setTimeout(() => setShowModalActive(true), 600)
}}
/>
+ {canManageApprover && (
+ }
+ title={isApprover ? "Revoke Approver" : "Jadikan Approver"}
+ color={colors.text}
+ onPress={() => {
+ setVisible(false)
+ setTimeout(() => setShowModalApprover(true), 600)
+ }}
+ />
+ )}
{
- setShowModal(false)
+ setShowModalActive(false)
handleActive()
}}
- onCancel={() => setShowModal(false)}
+ onCancel={() => setShowModalActive(false)}
+ confirmText="Konfirmasi"
+ cancelText="Batal"
+ />
+
+ setShowModalApprover(false)}
confirmText="Konfirmasi"
cancelText="Batal"
/>
diff --git a/components/project/sectionTanggalTugas.tsx b/components/project/sectionTanggalTugas.tsx
index 9b553d8..19483f6 100644
--- a/components/project/sectionTanggalTugas.tsx
+++ b/components/project/sectionTanggalTugas.tsx
@@ -1,5 +1,5 @@
import Styles from "@/constants/Styles";
-import { apiDeleteProjectTask, apiGetProjectOne, apiUpdateStatusProjectTask } from "@/lib/api";
+import { apiApproveRejectProjectTask, apiDeleteProjectTask, apiGetProjectOne, apiGetProjectTaskApprovals, apiSubmitProjectTask } from "@/lib/api";
import { setUpdateProject } from "@/lib/projectUpdate";
import { useAuthSession } from "@/providers/AuthProvider";
import { useTheme } from "@/providers/ThemeProvider";
@@ -13,7 +13,8 @@ import DrawerBottom from "../drawerBottom";
import ItemSectionTanggalTugas from "../itemSectionTanggalTugas";
import MenuItemRow from "../menuItemRow";
import ModalConfirmation from "../ModalConfirmation";
-import ModalSelect from "../modalSelect";
+import ModalRiwayatApproval from "../ModalRiwayatApproval";
+import ModalTolakApproval from "../ModalTolakApproval";
import SkeletonTask from "../skeletonTask";
import Text from "../Text";
import ModalListDetailTugasProject from "./modalListDetailTugasProject";
@@ -22,41 +23,52 @@ type Props = {
id: string;
title: string;
desc: string;
- status: 1;
+ status: number;
dateStart: string;
dateEnd: string;
createdAt: string;
files?: { name: string; extension: string }[];
};
+type ApprovalRecord = {
+ id: string
+ status: number
+ note?: string
+ submitter: { name: string }
+ approver?: { name: string }
+ createdAt: string
+}
+
export default function SectionTanggalTugasProject({ status, member, refreshing }: { status: number | undefined, member: boolean, refreshing?: boolean }) {
const { colors } = useTheme();
const entityUser = useSelector((state: any) => state.user)
const dispatch = useDispatch()
const update = useSelector((state: any) => state.projectUpdate)
const [isModal, setModal] = useState(false);
- const [isSelect, setSelect] = useState(false);
const { token, decryptToken } = useAuthSession();
const [modalDetail, setModalDetail] = useState(false)
+ const [modalRiwayat, setModalRiwayat] = useState(false)
+ const [modalTolak, setModalTolak] = useState(false)
+ const [modalKonfirmasiSetujui, setModalKonfirmasiSetujui] = useState(false)
+ const [modalPersetujuan, setModalPersetujuan] = useState(false)
const { id } = useLocalSearchParams<{ id: string }>();
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true)
+ const [loadingAction, setLoadingAction] = useState(false)
+ const [loadingRiwayat, setLoadingRiwayat] = useState(false)
+ const [riwayatData, setRiwayatData] = useState([])
const arrSkeleton = Array.from({ length: 5 });
- const [tugas, setTugas] = useState({
- id: '',
- status: 0,
- })
+ const [tugas, setTugas] = useState({ id: '', status: 0 })
const [showDeleteModal, setShowDeleteModal] = useState(false)
+ const isApprover = entityUser.isApprover || ['supadmin', 'developer'].includes(entityUser.role)
+ const isAdmin = entityUser.role !== 'user' && entityUser.role !== 'coadmin'
+
async function handleLoad(loading: boolean) {
try {
setLoading(loading)
const hasil = await decryptToken(String(token?.current));
- const response = await apiGetProjectOne({
- user: hasil,
- cat: "task",
- id: id,
- });
+ const response = await apiGetProjectOne({ user: hasil, cat: "task", id: id });
setData(response.data);
} catch (error) {
console.error(error);
@@ -65,178 +77,268 @@ export default function SectionTanggalTugasProject({ status, member, refreshing
}
}
- useEffect(() => {
- handleLoad(false);
- }, [update.task]);
+ useEffect(() => { handleLoad(false) }, [update.task]);
+ useEffect(() => { if (refreshing) handleLoad(false) }, [refreshing]);
+ useEffect(() => { handleLoad(true) }, []);
- useEffect(() => {
- if (refreshing)
- handleLoad(false);
- }, [refreshing]);
-
- useEffect(() => {
- handleLoad(true);
- }, []);
-
-
- async function handleUpdate(status: number) {
+ async function handleLoadRiwayat() {
try {
- const hasil = await decryptToken(String(token?.current));
- const response = await apiUpdateStatusProjectTask({
- user: hasil,
- idProject: id,
- status: status,
- }, tugas.id);
+ setLoadingRiwayat(true)
+ const hasil = await decryptToken(String(token?.current))
+ const response = await apiGetProjectTaskApprovals({ user: hasil, id: tugas.id })
+ setRiwayatData(response.data ?? [])
+ } catch (error) {
+ console.error(error)
+ } finally {
+ setLoadingRiwayat(false)
+ }
+ }
+
+ async function handleSubmitAjukan() {
+ try {
+ setLoadingAction(true)
+ const hasil = await decryptToken(String(token?.current))
+ const response = await apiSubmitProjectTask({ user: hasil, id: tugas.id })
if (response.success) {
dispatch(setUpdateProject({ ...update, progress: !update.progress, task: !update.task }))
- setSelect(false);
- Toast.show({ type: 'small', text1: 'Berhasil mengubah data', })
+ Toast.show({ type: 'small', text1: 'Berhasil mengajukan task untuk persetujuan' })
+ } else {
+ Toast.show({ type: 'small', text1: response.message })
}
} catch (error: any) {
- console.error(error);
- const message = error?.response?.data?.message || "Gagal mengubah data"
-
+ const message = error?.response?.data?.message || "Gagal mengajukan persetujuan"
Toast.show({ type: 'small', text1: message })
+ } finally {
+ setLoadingAction(false)
+ setModal(false)
+ }
+ }
+
+ async function handleSetujui() {
+ try {
+ setLoadingAction(true)
+ const hasil = await decryptToken(String(token?.current))
+ const response = await apiApproveRejectProjectTask({ user: hasil, id: tugas.id, action: 'approve' })
+ if (response.success) {
+ dispatch(setUpdateProject({ ...update, progress: !update.progress, task: !update.task }))
+ Toast.show({ type: 'small', text1: 'Tugas berhasil disetujui' })
+ } else {
+ Toast.show({ type: 'small', text1: response.message })
+ }
+ } catch (error: any) {
+ const message = error?.response?.data?.message || "Gagal menyetujui tugas"
+ Toast.show({ type: 'small', text1: message })
+ } finally {
+ setLoadingAction(false)
+ setModalKonfirmasiSetujui(false)
+ }
+ }
+
+ async function handleTolak(note: string) {
+ try {
+ setLoadingAction(true)
+ const hasil = await decryptToken(String(token?.current))
+ const response = await apiApproveRejectProjectTask({ user: hasil, id: tugas.id, action: 'reject', note })
+ if (response.success) {
+ dispatch(setUpdateProject({ ...update, progress: !update.progress, task: !update.task }))
+ Toast.show({ type: 'small', text1: 'Tugas berhasil ditolak' })
+ } else {
+ Toast.show({ type: 'small', text1: response.message })
+ }
+ } catch (error: any) {
+ const message = error?.response?.data?.message || "Gagal menolak tugas"
+ Toast.show({ type: 'small', text1: message })
+ } finally {
+ setLoadingAction(false)
+ setModalTolak(false)
}
}
async function handleDelete() {
try {
const hasil = await decryptToken(String(token?.current));
- const response = await apiDeleteProjectTask({
- user: hasil,
- idProject: id,
- }, tugas.id);
+ const response = await apiDeleteProjectTask({ user: hasil, idProject: id }, tugas.id);
if (response.success) {
dispatch(setUpdateProject({ ...update, progress: !update.progress, task: !update.task }))
- setModal(false);
- Toast.show({ type: 'small', text1: 'Berhasil menghapus data', })
+ Toast.show({ type: 'small', text1: 'Berhasil menghapus data' })
}
} catch (error: any) {
- console.error(error);
const message = error?.response?.data?.message || "Gagal menghapus data"
-
Toast.show({ type: 'small', text1: message })
}
}
+ const canTakeAction = member || isAdmin
+ const showAjukan = (member || isApprover) && tugas.status === 0 && status !== 3
+ const showApproverActions = isApprover && tugas.status === 2
+
return (
<>
-
- Tanggal & Tugas
-
+ Tanggal & Tugas
- {
- loading ?
- arrSkeleton.map((item, index) => {
- return (
-
- )
- })
- :
- data.length > 0
- ?
- data.map((item, index) => {
- return (
- {
- setTugas({ id: item.id, status: item.status })
- setModal(true)
- }}
- />
- );
- })
- :
- Tidak ada tugas
+ {loading
+ ? arrSkeleton.map((_, index) => )
+ : data.length > 0
+ ? data.map((item, index) => (
+ {
+ setTugas({ id: item.id, status: item.status })
+ setModal(true)
+ }}
+ />
+ ))
+ : Tidak ada tugas
}
-
-
- {(member || (entityUser.role != "user" && entityUser.role != "coadmin")) && status != 3 && (
- }
- title="Update Status"
- onPress={() => {
- setModal(false);
- setTimeout(() => setSelect(true), 600)
- }}
- />
- )}
+ {/* Drawer menu */}
+
+
+
+ {/* Baris 1 — selalu tampil */}
}
title="File Tugas"
onPress={() => {
- setModal(false);
- router.push(`/project/${id}/tugas-file/${tugas.id}?member=${member}`);
+ setModal(false)
+ router.push(`/project/${id}/tugas-file/${tugas.id}?member=${member}`)
}}
/>
}
title="Detail Waktu"
onPress={() => {
- setModal(false);
+ setModal(false)
setTimeout(() => setModalDetail(true), 600)
}}
/>
-
- {(member || (entityUser.role != "user" && entityUser.role != "coadmin")) && status != 3 && (
-
+ }
+ title="Riwayat"
+ onPress={() => {
+ setModal(false)
+ handleLoadRiwayat()
+ setTimeout(() => setModalRiwayat(true), 600)
+ }}
+ />
+
+ {/* Separator antar baris */}
+ {(showAjukan || showApproverActions || (canTakeAction && isAdmin && status !== 3)) && (
+
+ )}
+
+ {/* Baris 2 — semua aksi kondisional dalam satu baris */}
+ {showAjukan && (
}
- title="Edit Tugas"
- onPress={() => {
- setModal(false);
- router.push(`/project/update/${tugas.id}`);
- }}
- />
- }
- title="Hapus Tugas"
+ icon={}
+ title="Ajukan Selesai"
+ disabled={loadingAction}
onPress={() => {
setModal(false)
- setTimeout(() => setShowDeleteModal(true), 600)
+ setTimeout(() => handleSubmitAjukan(), 600)
}}
/>
-
- )}
+ )}
+ {showApproverActions && (
+ }
+ title="Persetujuan"
+ disabled={loadingAction}
+ onPress={() => {
+ setModal(false)
+ setTimeout(() => setModalPersetujuan(true), 600)
+ }}
+ />
+ )}
+ {canTakeAction && isAdmin && status !== 3 && (
+ <>
+ }
+ title="Edit Tugas"
+ onPress={() => {
+ setModal(false)
+ router.push(`/project/update/${tugas.id}`)
+ }}
+ />
+ }
+ title="Hapus Tugas"
+ onPress={() => {
+ setModal(false)
+ setTimeout(() => setShowDeleteModal(true), 600)
+ }}
+ />
+ >
+ )}
+
+
+
+ {/* Drawer persetujuan */}
+
+
+ }
+ title="Setujui"
+ color={colors.success}
+ disabled={loadingAction}
+ onPress={() => {
+ setModalPersetujuan(false)
+ setTimeout(() => setModalKonfirmasiSetujui(true), 600)
+ }}
+ />
+ }
+ title="Tolak"
+ color={colors.error}
+ disabled={loadingAction}
+ onPress={() => {
+ setModalPersetujuan(false)
+ setTimeout(() => setModalTolak(true), 600)
+ }}
+ />
+
{
- setShowDeleteModal(false)
- handleDelete()
- }}
+ onConfirm={() => { setShowDeleteModal(false); handleDelete() }}
onCancel={() => setShowDeleteModal(false)}
confirmText="Hapus"
cancelText="Batal"
/>
- { setSelect(false) }}
- onSelect={(value) => {
- handleUpdate(Number(value.val))
- }}
- title="Status"
- open={isSelect}
- valChoose={String(tugas.status)}
+ setModalKonfirmasiSetujui(false)}
+ confirmText="Setujui"
+ cancelText="Batal"
+ />
+
+
+
+
0
? 'Sedang berlangsung'
- : 'Belum dimulai';
+ : 'Belum ada tugas yang diselesaikan';
useEffect(() => {
animatedWidth.setValue(0);
diff --git a/components/task/sectionTanggalTugasTask.tsx b/components/task/sectionTanggalTugasTask.tsx
index 34d48d4..5a4e013 100644
--- a/components/task/sectionTanggalTugasTask.tsx
+++ b/components/task/sectionTanggalTugasTask.tsx
@@ -1,5 +1,5 @@
import Styles from "@/constants/Styles";
-import { apiDeleteTaskTugas, apiGetTaskOne, apiUpdateStatusTaskDivision } from "@/lib/api";
+import { apiApproveRejectTaskTugas, apiDeleteTaskTugas, apiGetTaskOne, apiGetTaskTugasApprovals, apiSubmitTaskTugas } from "@/lib/api";
import { setUpdateTask } from "@/lib/taskUpdate";
import { useAuthSession } from "@/providers/AuthProvider";
import { useTheme } from "@/providers/ThemeProvider";
@@ -13,7 +13,8 @@ import DrawerBottom from "../drawerBottom";
import ItemSectionTanggalTugas from "../itemSectionTanggalTugas";
import MenuItemRow from "../menuItemRow";
import ModalConfirmation from "../ModalConfirmation";
-import ModalSelect from "../modalSelect";
+import ModalRiwayatApproval from "../ModalRiwayatApproval";
+import ModalTolakApproval from "../ModalTolakApproval";
import SkeletonTask from "../skeletonTask";
import Text from "../Text";
import ModalListDetailTugasTask from "./modalListDetailTugasTask";
@@ -28,25 +29,41 @@ type Props = {
files?: { name: string; extension: string }[];
}
-export default function SectionTanggalTugasTask({ refreshing, isMemberDivision }: { refreshing: boolean, isMemberDivision: boolean }) {
+type ApprovalRecord = {
+ id: string
+ status: number
+ note?: string
+ submitter: { name: string }
+ approver?: { name: string }
+ createdAt: string
+}
+
+export default function SectionTanggalTugasTask({ refreshing, isMemberDivision, isAdminDivision, status }: { refreshing: boolean, isMemberDivision: boolean, isAdminDivision: boolean, status?: number }) {
const { colors } = useTheme()
const dispatch = useDispatch()
const entityUser = useSelector((state: any) => state.user);
const update = useSelector((state: any) => state.taskUpdate)
const [isModal, setModal] = useState(false)
- const [isSelect, setSelect] = useState(false)
const { token, decryptToken } = useAuthSession()
const [loading, setLoading] = useState(true)
+ const [loadingAction, setLoadingAction] = useState(false)
+ const [loadingRiwayat, setLoadingRiwayat] = useState(false)
const arrSkeleton = Array.from({ length: 5 })
const [modalDetail, setModalDetail] = useState(false)
+ const [modalRiwayat, setModalRiwayat] = useState(false)
+ const [modalTolak, setModalTolak] = useState(false)
+ const [modalKonfirmasiSetujui, setModalKonfirmasiSetujui] = useState(false)
+ const [modalPersetujuan, setModalPersetujuan] = useState(false)
+ const [riwayatData, setRiwayatData] = useState([])
const { id, detail } = useLocalSearchParams<{ id: string, detail: string }>();
const [data, setData] = useState([])
- const [tugas, setTugas] = useState({
- id: '',
- status: 0,
- })
+ const [tugas, setTugas] = useState({ id: '', status: 0 })
const [showDeleteModal, setShowDeleteModal] = useState(false)
+ const isApprover = entityUser.isApprover || ['supadmin', 'developer'].includes(entityUser.role)
+ const isAdmin = entityUser.role !== 'user' && entityUser.role !== 'coadmin'
+ const canTakeAction = isMemberDivision || isAdmin
+
async function handleLoad(loading: boolean) {
try {
setLoading(loading)
@@ -60,126 +77,136 @@ export default function SectionTanggalTugasTask({ refreshing, isMemberDivision }
}
}
- async function handleUpdate(status: number) {
- try {
- const hasil = await decryptToken(String(token?.current));
- const response = await apiUpdateStatusTaskDivision({
- user: hasil,
- idProject: detail,
- status: status,
- }, tugas.id);
- if (response.success) {
- dispatch(setUpdateTask({ ...update, progress: !update.progress, task: !update.task }))
- Toast.show({ type: 'small', text1: 'Berhasil mengubah data', })
- } else {
- Toast.show({ type: 'small', text1: response.message, })
- }
- } catch (error: any) {
- console.error(error);
- const message = error?.response?.data?.message || "Gagal mengubah data"
+ useEffect(() => { handleLoad(false) }, [update.task])
+ useEffect(() => { if (refreshing) handleLoad(false) }, [refreshing])
+ useEffect(() => { handleLoad(true) }, [])
- Toast.show({ type: 'small', text1: message })
+ async function handleLoadRiwayat() {
+ try {
+ setLoadingRiwayat(true)
+ const hasil = await decryptToken(String(token?.current))
+ const response = await apiGetTaskTugasApprovals({ user: hasil, id: tugas.id })
+ setRiwayatData(response.data ?? [])
+ } catch (error) {
+ console.error(error)
} finally {
- setSelect(false)
+ setLoadingRiwayat(false)
}
}
- useEffect(() => {
- handleLoad(false)
- }, [update.task])
+ async function handleSubmitAjukan() {
+ try {
+ setLoadingAction(true)
+ const hasil = await decryptToken(String(token?.current))
+ const response = await apiSubmitTaskTugas({ user: hasil, id: tugas.id })
+ if (response.success) {
+ dispatch(setUpdateTask({ ...update, progress: !update.progress, task: !update.task }))
+ Toast.show({ type: 'small', text1: 'Berhasil mengajukan task untuk persetujuan' })
+ } else {
+ Toast.show({ type: 'small', text1: response.message })
+ }
+ } catch (error: any) {
+ Toast.show({ type: 'small', text1: error?.response?.data?.message || "Gagal mengajukan persetujuan" })
+ } finally {
+ setLoadingAction(false)
+ setModal(false)
+ }
+ }
- useEffect(() => {
- if (refreshing)
- handleLoad(false);
- }, [refreshing]);
-
-
- useEffect(() => {
- handleLoad(true)
- }, [])
+ async function handleSetujui() {
+ try {
+ setLoadingAction(true)
+ const hasil = await decryptToken(String(token?.current))
+ const response = await apiApproveRejectTaskTugas({ user: hasil, id: tugas.id, action: 'approve' })
+ if (response.success) {
+ dispatch(setUpdateTask({ ...update, progress: !update.progress, task: !update.task }))
+ Toast.show({ type: 'small', text1: 'Tugas berhasil disetujui' })
+ } else {
+ Toast.show({ type: 'small', text1: response.message })
+ }
+ } catch (error: any) {
+ Toast.show({ type: 'small', text1: error?.response?.data?.message || "Gagal menyetujui tugas" })
+ } finally {
+ setLoadingAction(false)
+ setModalKonfirmasiSetujui(false)
+ }
+ }
+ async function handleTolak(note: string) {
+ try {
+ setLoadingAction(true)
+ const hasil = await decryptToken(String(token?.current))
+ const response = await apiApproveRejectTaskTugas({ user: hasil, id: tugas.id, action: 'reject', note })
+ if (response.success) {
+ dispatch(setUpdateTask({ ...update, progress: !update.progress, task: !update.task }))
+ Toast.show({ type: 'small', text1: 'Tugas berhasil ditolak' })
+ } else {
+ Toast.show({ type: 'small', text1: response.message })
+ }
+ } catch (error: any) {
+ Toast.show({ type: 'small', text1: error?.response?.data?.message || "Gagal menolak tugas" })
+ } finally {
+ setLoadingAction(false)
+ setModalTolak(false)
+ }
+ }
async function handleDelete() {
try {
const hasil = await decryptToken(String(token?.current));
- const response = await apiDeleteTaskTugas({
- user: hasil,
- idProject: detail,
- }, tugas.id);
+ const response = await apiDeleteTaskTugas({ user: hasil, idProject: detail }, tugas.id);
if (response.success) {
dispatch(setUpdateTask({ ...update, progress: !update.progress, task: !update.task }))
- Toast.show({ type: 'small', text1: 'Berhasil menghapus data', })
+ Toast.show({ type: 'small', text1: 'Berhasil menghapus data' })
} else {
- Toast.show({ type: 'small', text1: response.message, })
+ Toast.show({ type: 'small', text1: response.message })
}
} catch (error: any) {
- console.error(error);
- const message = error?.response?.data?.message || "Gagal menghapus data"
-
- Toast.show({ type: 'small', text1: message })
- } finally {
- setModal(false);
+ Toast.show({ type: 'small', text1: error?.response?.data?.message || "Gagal menghapus data" })
}
}
+ const canApprove = isApprover || isAdminDivision
+ const showAjukan = (isMemberDivision || canApprove) && tugas.status === 0 && status !== 3
+ const showApproverActions = canApprove && tugas.status === 2
return (
<>
Tanggal & Tugas
- {
- loading ?
- arrSkeleton.map((item, index) => {
- return (
-
- )
- })
- :
- data.length > 0
- ?
- data.map((item, index) => {
- return (
- {
- setTugas({
- id: item.id,
- status: item.status
- })
- setModal(true)
- }}
- />
- );
- })
- :
- Tidak ada tugas
+ {loading
+ ? arrSkeleton.map((_, index) => )
+ : data.length > 0
+ ? data.map((item, index) => (
+ {
+ setTugas({ id: item.id, status: item.status })
+ setModal(true)
+ }}
+ />
+ ))
+ : Tidak ada tugas
}
-
-
- {((entityUser.role != "user" && entityUser.role != "coadmin") || isMemberDivision) && (
- }
- title="Update Status"
- onPress={() => {
- setModal(false)
- setTimeout(() => setSelect(true), 600)
- }}
- />
- )}
+ {/* Drawer menu */}
+
+
+
+ {/* Baris 1 — selalu tampil */}
}
title="File Tugas"
onPress={() => {
- setModal(false);
+ setModal(false)
router.push(`/division/${id}/task/${detail}/tugas-file/${tugas.id}?member=${isMemberDivision}`)
}}
/>
@@ -187,53 +214,129 @@ export default function SectionTanggalTugasTask({ refreshing, isMemberDivision }
icon={}
title="Detail Waktu"
onPress={() => {
- setModal(false);
+ setModal(false)
setTimeout(() => setModalDetail(true), 600)
}}
/>
+ }
+ title="Riwayat"
+ onPress={() => {
+ setModal(false)
+ handleLoadRiwayat()
+ setTimeout(() => setModalRiwayat(true), 600)
+ }}
+ />
+
+ {/* Separator */}
+ {(showAjukan || showApproverActions || (canTakeAction && isAdmin && status !== 3)) && (
+
+ )}
+
+ {/* Baris 2 — aksi kondisional */}
+ {showAjukan && (
+ }
+ title="Ajukan Selesai"
+ disabled={loadingAction}
+ onPress={() => {
+ setModal(false)
+ setTimeout(() => handleSubmitAjukan(), 600)
+ }}
+ />
+ )}
+ {showApproverActions && (
+ }
+ title="Persetujuan"
+ disabled={loadingAction}
+ onPress={() => {
+ setModal(false)
+ setTimeout(() => setModalPersetujuan(true), 600)
+ }}
+ />
+ )}
+ {canTakeAction && isAdmin && status !== 3 && (
+ <>
+ }
+ title="Edit Tugas"
+ onPress={() => {
+ setModal(false)
+ router.push(`./update/${tugas.id}`)
+ }}
+ />
+ }
+ title="Hapus Tugas"
+ onPress={() => {
+ setModal(false)
+ setTimeout(() => setShowDeleteModal(true), 600)
+ }}
+ />
+ >
+ )}
+
+
+
+ {/* Drawer persetujuan */}
+
+
+ }
+ title="Setujui"
+ color={colors.success}
+ disabled={loadingAction}
+ onPress={() => {
+ setModalPersetujuan(false)
+ setTimeout(() => setModalKonfirmasiSetujui(true), 600)
+ }}
+ />
+ }
+ title="Tolak"
+ color={colors.error}
+ disabled={loadingAction}
+ onPress={() => {
+ setModalPersetujuan(false)
+ setTimeout(() => setModalTolak(true), 600)
+ }}
+ />
- {((entityUser.role != "user" && entityUser.role != "coadmin") || isMemberDivision) && (
-
- }
- title="Edit Tugas"
- onPress={() => {
- setModal(false)
- router.push(`./update/${tugas.id}`)
- }}
- />
- }
- title="Hapus Tugas"
- onPress={() => {
- setModal(false)
- setTimeout(() => setShowDeleteModal(true), 600)
- }}
- />
-
- )}
{
- setShowDeleteModal(false)
- handleDelete()
- }}
+ onConfirm={() => { setShowDeleteModal(false); handleDelete() }}
onCancel={() => setShowDeleteModal(false)}
confirmText="Hapus"
cancelText="Batal"
/>
- setSelect(false)}
- onSelect={(value) => { handleUpdate(Number(value.val)) }}
- title="Status"
- open={isSelect}
- valChoose={String(tugas.status)}
+ setModalKonfirmasiSetujui(false)}
+ confirmText="Setujui"
+ cancelText="Batal"
+ />
+
+
+
+
>
)
-}
\ No newline at end of file
+}
diff --git a/constants/Styles.ts b/constants/Styles.ts
index 976a1aa..b9fab98 100644
--- a/constants/Styles.ts
+++ b/constants/Styles.ts
@@ -1068,6 +1068,51 @@ const Styles = StyleSheet.create({
color: 'white',
fontWeight: '500',
},
+ pv14: {
+ paddingVertical: 14,
+ },
+ mb08: {
+ marginBottom: 8,
+ },
+ cWhiteDimmed: {
+ color: 'rgba(255,255,255,0.7)',
+ },
+ memberAvatarRing: {
+ borderWidth: 3,
+ borderColor: 'rgba(255,255,255,0.4)',
+ borderRadius: 100,
+ },
+ memberBadgeRow: {
+ flexDirection: 'row',
+ gap: 8,
+ marginTop: 12,
+ },
+ memberBadgeApprover: {
+ paddingHorizontal: 10,
+ paddingVertical: 4,
+ borderRadius: 20,
+ borderWidth: 1,
+ borderColor: 'rgba(255,255,255,0.6)',
+ backgroundColor: 'rgba(255,255,255,0.15)',
+ },
+ memberBadgePill: {
+ paddingHorizontal: 10,
+ paddingVertical: 4,
+ borderRadius: 20,
+ },
+ memberInfoRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingVertical: 14,
+ },
+ memberInfoIcon: {
+ width: 36,
+ alignItems: 'center',
+ },
+ memberInfoContent: {
+ flex: 1,
+ marginLeft: 10,
+ },
})
export default Styles;
\ No newline at end of file
diff --git a/lib/api.ts b/lib/api.ts
index a744165..17a3967 100644
--- a/lib/api.ts
+++ b/lib/api.ts
@@ -166,6 +166,11 @@ export const apiDeleteUser = async (data: { user: string, isActive: boolean }, i
return response.data
};
+export const apiToggleApprover = async (data: { user: string, isApprover: boolean }, id: string) => {
+ const response = await api.patch(`mobile/user/${id}`, data)
+ return response.data
+};
+
export const apiEditUser = async (data: FormData, id: string) => {
const response = await api.put(`/mobile/user/${id}`, data, {
headers: {
@@ -379,6 +384,21 @@ export const apiDeleteProjectTaskFile = async (data: { user: string }, id: strin
return response.data;
};
+export const apiGetProjectTaskApprovals = async ({ user, id }: { user: string, id: string }) => {
+ const response = await api.get(`/mobile/project/task/${id}/approval`, { params: { user } })
+ return response.data;
+};
+
+export const apiSubmitProjectTask = async ({ user, id }: { user: string, id: string }) => {
+ const response = await api.post(`/mobile/project/task/${id}/approval`, { user })
+ return response.data;
+};
+
+export const apiApproveRejectProjectTask = async ({ user, id, action, note }: { user: string, id: string, action: 'approve' | 'reject', note?: string }) => {
+ const response = await api.put(`/mobile/project/task/${id}/approval`, { user, action, note })
+ return response.data;
+};
+
export const apiAddMemberProject = async ({ data, id }: { data: { user: string, member: any[] }, id: string }) => {
const response = await api.post(`/mobile/project/${id}/member`, data)
@@ -686,6 +706,21 @@ export const apiAddFileTask = async ({ data, id }: { data: FormData, id: string
return response.data;
};
+export const apiGetTaskTugasApprovals = async ({ user, id }: { user: string, id: string }) => {
+ const response = await api.get(`/mobile/task/tugas/${id}/approval`, { params: { user } })
+ return response.data;
+};
+
+export const apiSubmitTaskTugas = async ({ user, id }: { user: string, id: string }) => {
+ const response = await api.post(`/mobile/task/tugas/${id}/approval`, { user })
+ return response.data;
+};
+
+export const apiApproveRejectTaskTugas = async ({ user, id, action, note }: { user: string, id: string, action: 'approve' | 'reject', note?: string }) => {
+ const response = await api.put(`/mobile/task/tugas/${id}/approval`, { user, action, note })
+ return response.data;
+};
+
export const apiGetTugasTaskFile = async ({ user, id }: { user: string, id: string }) => {
const response = await api.get(`/mobile/task/tugas/file/${id}`, { params: { user } })
return response.data;