amalia/07-mei-26 #45
@@ -154,7 +154,7 @@ export default function DetailTaskDivision() {
|
||||
}
|
||||
<SectionProgress progress={progress} doneCount={taskStats?.done} totalCount={taskStats?.total} />
|
||||
<SectionReportTask refreshing={refreshing} />
|
||||
<SectionTanggalTugasTask refreshing={refreshing} isMemberDivision={isMemberDivision} />
|
||||
<SectionTanggalTugasTask refreshing={refreshing} isMemberDivision={isMemberDivision} isAdminDivision={isAdminDivision} status={data?.status} />
|
||||
<SectionFileTask refreshing={refreshing} isMemberDivision={isMemberDivision} />
|
||||
<SectionLinkTask refreshing={refreshing} isMemberDivision={isMemberDivision} />
|
||||
<SectionMemberTask refreshing={refreshing} isAdminDivision={isAdminDivision} />
|
||||
|
||||
@@ -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<Props[]>([])
|
||||
const [search, setSearch] = useState("")
|
||||
const update = useSelector((state: any) => state.taskUpdate)
|
||||
@@ -172,13 +185,9 @@ export default function ListTask() {
|
||||
n={4}
|
||||
/>
|
||||
</ScrollView>
|
||||
<View style={[Styles.rowSpaceBetween]}>
|
||||
<View style={[Styles.rowSpaceBetween, { alignItems: 'center' }]}>
|
||||
<InputSearch width={68} onChange={setSearch} />
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
setList(!isList);
|
||||
}}
|
||||
>
|
||||
<Pressable onPress={toggleView}>
|
||||
<MaterialCommunityIcons
|
||||
name={isList ? "format-list-bulleted" : "view-grid"}
|
||||
color={colors.text}
|
||||
@@ -219,9 +228,10 @@ export default function ListTask() {
|
||||
router.push(`./task/${item.id}`);
|
||||
}}
|
||||
borderType="bottom"
|
||||
bgColor="transparent"
|
||||
icon={
|
||||
<View style={[Styles.iconContent, ColorsStatus.lightGreen]}>
|
||||
<AntDesign name="areachart" size={25} color={"#384288"} />
|
||||
<View style={[Styles.iconContent]}>
|
||||
<AntDesign name="areachart" size={25} color={"black"} />
|
||||
</View>
|
||||
}
|
||||
title={item.title}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
<View style={[Styles.rowSpaceBetween, { alignItems: 'center' }]}>
|
||||
<InputSearch width={68} onChange={setSearch} />
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
setList(!isList);
|
||||
}}
|
||||
>
|
||||
<Pressable onPress={toggleView}>
|
||||
<MaterialCommunityIcons
|
||||
name={isList ? "format-list-bulleted" : "view-grid"}
|
||||
color={colors.text}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import AppHeader from "@/components/AppHeader";
|
||||
import ImageUser from "@/components/imageNew";
|
||||
import ItemDetailMember from "@/components/itemDetailMember";
|
||||
import LabelStatus from "@/components/labelStatus";
|
||||
import HeaderRightMemberDetail from "@/components/member/headerMemberDetail";
|
||||
import Skeleton from "@/components/skeleton";
|
||||
@@ -11,6 +10,7 @@ import { valueRoleUser } from "@/constants/RoleUser";
|
||||
import Styles from "@/constants/Styles";
|
||||
import { apiGetProfile } from "@/lib/api";
|
||||
import { useTheme } from "@/providers/ThemeProvider";
|
||||
import { AntDesign, MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons";
|
||||
import { LinearGradient } from "expo-linear-gradient";
|
||||
import { router, Stack, useLocalSearchParams } from "expo-router";
|
||||
import React, { useEffect, useState } from "react";
|
||||
@@ -30,6 +30,7 @@ type Props = {
|
||||
group: string,
|
||||
img: string,
|
||||
isActive: boolean,
|
||||
isApprover: boolean,
|
||||
role: string
|
||||
}
|
||||
|
||||
@@ -89,7 +90,7 @@ export default function MemberDetail() {
|
||||
showBack={true}
|
||||
onPressLeft={() => router.back()}
|
||||
right={
|
||||
(entityUser.role != "user") && isEdit ? <HeaderRightMemberDetail active={data?.isActive} id={id} /> : <></>
|
||||
(entityUser.role != "user") && isEdit ? <HeaderRightMemberDetail active={data?.isActive} id={id} isApprover={data?.isApprover ?? false} /> : <></>
|
||||
}
|
||||
/>
|
||||
)
|
||||
@@ -109,51 +110,71 @@ export default function MemberDetail() {
|
||||
colors={[colors.header, colors.homeGradient]}
|
||||
style={[Styles.wrapHeadViewMember]}
|
||||
>
|
||||
{
|
||||
loading ?
|
||||
<>
|
||||
<Skeleton width={100} height={100} borderRadius={100} />
|
||||
<Skeleton width={200} height={10} borderRadius={5} />
|
||||
<Skeleton width={150} height={10} borderRadius={5} />
|
||||
</>
|
||||
:
|
||||
<>
|
||||
<Pressable onPress={() => setPreview(true)}>
|
||||
{loading ? (
|
||||
<>
|
||||
<Skeleton width={100} height={100} borderRadius={100} />
|
||||
<Skeleton width={200} height={10} borderRadius={5} />
|
||||
<Skeleton width={150} height={10} borderRadius={5} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Pressable onPress={() => setPreview(true)}>
|
||||
<View style={[Styles.memberAvatarRing]}>
|
||||
<ImageUser src={`${ConstEnv.url_storage}/files/${data?.img}`} size="lg" onError={setErrorImg} />
|
||||
</Pressable>
|
||||
<Text style={[Styles.textSubtitle, Styles.cWhite, Styles.mt10, Styles.textCenter]}>{data?.name}</Text>
|
||||
<Text style={[Styles.textMediumNormal, Styles.cWhite]}>{data?.role}</Text>
|
||||
</>
|
||||
|
||||
}
|
||||
</View>
|
||||
</Pressable>
|
||||
<Text style={[Styles.textSubtitle, Styles.cWhite, Styles.mt10, Styles.textCenter]}>{data?.name}</Text>
|
||||
<Text style={[Styles.textMediumNormal, Styles.cWhiteDimmed]}>{data?.role}</Text>
|
||||
<View style={[Styles.memberBadgeRow]}>
|
||||
{data?.isApprover && (
|
||||
<View style={[Styles.memberBadgeApprover]}>
|
||||
<Text style={[Styles.textSmallSemiBold, Styles.cWhite]}>APPROVER</Text>
|
||||
</View>
|
||||
)}
|
||||
<View style={[Styles.memberBadgePill, { backgroundColor: data?.isActive ? colors.success : colors.error }]}>
|
||||
<Text style={[Styles.textSmallSemiBold, Styles.cWhite]}>{data?.isActive ? 'AKTIF' : 'TIDAK AKTIF'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</LinearGradient>
|
||||
<View style={[Styles.p15]}>
|
||||
<View style={[Styles.rowSpaceBetween]}>
|
||||
<Text style={[Styles.textDefaultSemiBold, { color: colors.text }]}>Informasi</Text>
|
||||
<LabelStatus
|
||||
size="small"
|
||||
category={data?.isActive ? 'success' : 'error'}
|
||||
text={data?.isActive ? 'AKTIF' : 'TIDAK AKTIF'}
|
||||
/>
|
||||
</View>
|
||||
{
|
||||
loading ?
|
||||
arrSkeleton.map((item, index) => {
|
||||
return (
|
||||
<Skeleton key={index} width={100} widthType="percent" height={25} borderRadius={5} />
|
||||
)
|
||||
})
|
||||
:
|
||||
<>
|
||||
<ItemDetailMember category="nik" value={data?.nik} />
|
||||
<ItemDetailMember category="group" value={data?.group} />
|
||||
<ItemDetailMember category="position" value={data?.position} />
|
||||
<ItemDetailMember category="phone" value={`+62${data?.phone}`} />
|
||||
<ItemDetailMember category="email" value={data?.email} />
|
||||
<ItemDetailMember category="gender" value={data?.gender == "F" ? "Perempuan" : "Laki-Laki"} />
|
||||
</>
|
||||
}
|
||||
|
||||
<View style={[Styles.p15]}>
|
||||
<Text style={[Styles.textDefaultSemiBold, Styles.mb08, { color: colors.dimmed }]}>Informasi</Text>
|
||||
<View>
|
||||
{loading ? (
|
||||
arrSkeleton.map((_, index) => (
|
||||
<View key={index} style={[Styles.pv14, { borderBottomWidth: index < arrSkeleton.length - 1 ? 1 : 0, borderBottomColor: `${colors.dimmed}30` }]}>
|
||||
<Skeleton width={80} height={8} borderRadius={4} />
|
||||
<View style={[Styles.mt05]}>
|
||||
<Skeleton width={60} widthType="percent" height={10} borderRadius={4} />
|
||||
</View>
|
||||
</View>
|
||||
))
|
||||
) : (
|
||||
[
|
||||
{ icon: <MaterialCommunityIcons name="card-account-details" size={20} color={colors.icon} />, label: 'NIK', value: data?.nik },
|
||||
{ icon: <MaterialCommunityIcons name="office-building-outline" size={20} color={colors.icon} />, label: 'Lembaga Desa', value: data?.group },
|
||||
{ icon: <MaterialCommunityIcons name="account-tie" size={20} color={colors.icon} />, label: 'Jabatan', value: data?.position },
|
||||
{ icon: <MaterialIcons name="phone" size={20} color={colors.icon} />, label: 'No Telepon', value: `+62${data?.phone}` },
|
||||
{ icon: <MaterialIcons name="email" size={20} color={colors.icon} />, label: 'Email', value: data?.email },
|
||||
{ icon: <MaterialCommunityIcons name="gender-male-female" size={20} color={colors.icon} />, label: 'Jenis Kelamin', value: data?.gender == "F" ? "Perempuan" : "Laki-Laki" },
|
||||
].map((item, index, arr) => (
|
||||
<View
|
||||
key={index}
|
||||
style={[Styles.memberInfoRow, { borderBottomWidth: index < arr.length - 1 ? 1 : 0, borderBottomColor: `${colors.dimmed}30` }]}
|
||||
>
|
||||
<View style={[Styles.memberInfoIcon]}>
|
||||
{item.icon}
|
||||
</View>
|
||||
<View style={[Styles.memberInfoContent]}>
|
||||
<Text style={[Styles.textInformation, { color: colors.dimmed }]}>{item.label}</Text>
|
||||
<Text style={[Styles.textDefault, Styles.mt02, { color: colors.text }]} numberOfLines={1}>{item.value ?? '-'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
|
||||
@@ -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: <MaterialCommunityIcons name="card-account-details" size={20} color={colors.icon} />, label: 'NIK', value: entities.nik },
|
||||
{ icon: <MaterialCommunityIcons name="office-building-outline" size={20} color={colors.icon} />, label: 'Lembaga Desa', value: entities.group },
|
||||
{ icon: <MaterialCommunityIcons name="account-tie" size={20} color={colors.icon} />, label: 'Jabatan', value: entities.position },
|
||||
{ icon: <MaterialIcons name="phone" size={20} color={colors.icon} />, label: 'No Telepon', value: `0${entities.phone}` },
|
||||
{ icon: <MaterialIcons name="email" size={20} color={colors.icon} />, label: 'Email', value: entities.email },
|
||||
{ icon: <MaterialCommunityIcons name="gender-male-female" size={20} color={colors.icon} />, label: 'Jenis Kelamin', value: entities.gender == "F" ? 'Perempuan' : 'Laki-laki' },
|
||||
]
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[Styles.flex1, { backgroundColor: colors.background }]}>
|
||||
<Stack.Screen
|
||||
@@ -56,9 +64,7 @@ export default function Profile() {
|
||||
right={
|
||||
<ButtonHeader
|
||||
item={<Feather name="settings" size={20} color="white" />}
|
||||
onPress={() => {
|
||||
router.push('/setting')
|
||||
}}
|
||||
onPress={() => router.push('/setting')}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
@@ -75,32 +81,47 @@ export default function Profile() {
|
||||
}
|
||||
style={[Styles.h100, { backgroundColor: colors.background }]}
|
||||
>
|
||||
<View style={[Styles.flexColumn]}>
|
||||
<LinearGradient
|
||||
colors={[colors.header, colors.homeGradient]}
|
||||
style={[Styles.wrapHeadViewMember]}
|
||||
>
|
||||
<Pressable onPress={() => setPreview(true)}>
|
||||
<LinearGradient
|
||||
colors={[colors.header, colors.homeGradient]}
|
||||
style={[Styles.wrapHeadViewMember]}
|
||||
>
|
||||
<Pressable onPress={() => setPreview(true)}>
|
||||
<View style={[Styles.memberAvatarRing]}>
|
||||
<Image
|
||||
source={error ? require("../../assets/images/user.jpg") : { uri: `${ConstEnv.url_storage}/files/${entities.img}` }}
|
||||
onError={() => { setError(true) }}
|
||||
onError={() => setError(true)}
|
||||
style={[Styles.userProfileBig]}
|
||||
/>
|
||||
</Pressable>
|
||||
<Text style={[Styles.textSubtitle, Styles.cWhite, Styles.mt10]}>{entities.name}</Text>
|
||||
<Text style={[Styles.textMediumNormal, Styles.cWhite]}>{entities.role}</Text>
|
||||
</LinearGradient>
|
||||
<View style={[Styles.p15]}>
|
||||
<View style={[Styles.rowSpaceBetween]}>
|
||||
<Text style={[Styles.textDefaultSemiBold, { color: colors.text }]}>Informasi</Text>
|
||||
</View>
|
||||
{/* Note: ItemDetailMember might need updates to support dynamic colors if it uses default text colors */}
|
||||
<ItemDetailMember category="nik" value={entities.nik} />
|
||||
<ItemDetailMember category="group" value={entities.group} />
|
||||
<ItemDetailMember category="position" value={entities.position} />
|
||||
<ItemDetailMember category="phone" value={`0${entities.phone}`} />
|
||||
<ItemDetailMember category="email" value={entities.email} />
|
||||
<ItemDetailMember category="gender" value={entities.gender == "F" ? 'Perempuan' : 'Laki-laki'} />
|
||||
</Pressable>
|
||||
<Text style={[Styles.textSubtitle, Styles.cWhite, Styles.mt10, Styles.textCenter]}>{entities.name}</Text>
|
||||
<Text style={[Styles.textMediumNormal, Styles.cWhiteDimmed]}>{entities.role}</Text>
|
||||
{entities.isApprover && (
|
||||
<View style={[Styles.memberBadgeRow, { justifyContent: 'center' }]}>
|
||||
<View style={[Styles.memberBadgeApprover]}>
|
||||
<Text style={[Styles.textSmallSemiBold, Styles.cWhite]}>APPROVER</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</LinearGradient>
|
||||
|
||||
<View style={[Styles.p15]}>
|
||||
<Text style={[Styles.textDefaultSemiBold, Styles.mb08, { color: colors.dimmed }]}>Informasi</Text>
|
||||
<View>
|
||||
{infoRows.map((item, index, arr) => (
|
||||
<View
|
||||
key={index}
|
||||
style={[Styles.memberInfoRow, { borderBottomWidth: index < arr.length - 1 ? 1 : 0, borderBottomColor: `${colors.dimmed}30` }]}
|
||||
>
|
||||
<View style={[Styles.memberInfoIcon]}>
|
||||
{item.icon}
|
||||
</View>
|
||||
<View style={[Styles.memberInfoContent]}>
|
||||
<Text style={[Styles.textInformation, { color: colors.dimmed }]}>{item.label}</Text>
|
||||
<Text style={[Styles.textDefault, Styles.mt02, { color: colors.text }]} numberOfLines={1}>{item.value ?? '-'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
@@ -114,4 +135,4 @@ export default function Profile() {
|
||||
/>
|
||||
</SafeAreaView>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
<View style={[Styles.rowSpaceBetween, Styles.rowItemsCenter]}>
|
||||
<InputSearch width={68} onChange={setSearch} />
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
setList(!isList);
|
||||
}}
|
||||
>
|
||||
<Pressable onPress={toggleView}>
|
||||
<MaterialCommunityIcons
|
||||
name={isList ? "format-list-bulleted" : "view-grid"}
|
||||
color={colors.text}
|
||||
|
||||
140
components/ModalRiwayatApproval.tsx
Normal file
140
components/ModalRiwayatApproval.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import Styles from "@/constants/Styles"
|
||||
import { useTheme } from "@/providers/ThemeProvider"
|
||||
import { MaterialCommunityIcons } from "@expo/vector-icons"
|
||||
import { useRef, useState } from "react"
|
||||
import { ScrollView, View } from "react-native"
|
||||
import DrawerBottom from "./drawerBottom"
|
||||
import Skeleton from "./skeleton"
|
||||
import Text from "./Text"
|
||||
|
||||
type ApprovalRecord = {
|
||||
id: string
|
||||
status: number // 0=pending, 1=approved, 2=rejected
|
||||
note?: string
|
||||
submitter: { name: string }
|
||||
approver?: { name: string }
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
type Props = {
|
||||
isVisible: boolean
|
||||
setVisible: (value: boolean) => 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 (
|
||||
<View style={{
|
||||
backgroundColor: config.color + '20',
|
||||
borderRadius: 20,
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 3,
|
||||
alignSelf: 'flex-start',
|
||||
}}>
|
||||
<Text style={[Styles.textSmallSemiBold, { color: config.color }]}>
|
||||
{config.label}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default function ModalRiwayatApproval({ isVisible, setVisible, data, loading }: Props) {
|
||||
const { colors } = useTheme()
|
||||
const arrSkeleton = Array.from({ length: 3 })
|
||||
const scrollRef = useRef<ScrollView>(null)
|
||||
const [scrollOffset, setScrollOffset] = useState(0)
|
||||
|
||||
return (
|
||||
<DrawerBottom
|
||||
isVisible={isVisible}
|
||||
setVisible={setVisible}
|
||||
title="Riwayat Persetujuan"
|
||||
animation="slide"
|
||||
height={60}
|
||||
scrollOffset={scrollOffset}
|
||||
scrollTo={(p) => scrollRef.current?.scrollTo(p)}
|
||||
>
|
||||
<ScrollView
|
||||
ref={scrollRef}
|
||||
showsVerticalScrollIndicator={false}
|
||||
onScroll={({ nativeEvent }) => setScrollOffset(nativeEvent.contentOffset.y)}
|
||||
scrollEventThrottle={16}
|
||||
>
|
||||
{loading ? (
|
||||
arrSkeleton.map((_, i) => (
|
||||
<View key={i} style={[Styles.mb10]}>
|
||||
<Skeleton width={100} widthType="percent" height={80} borderRadius={10} />
|
||||
</View>
|
||||
))
|
||||
) : data.length > 0 ? (
|
||||
data.map((item, index) => (
|
||||
<View
|
||||
key={item.id}
|
||||
style={{
|
||||
borderWidth: 1,
|
||||
borderColor: colors.icon + '30',
|
||||
borderRadius: 10,
|
||||
padding: 12,
|
||||
marginBottom: 10,
|
||||
}}
|
||||
>
|
||||
{/* Status + tanggal */}
|
||||
<View style={[Styles.rowItemsCenter, { justifyContent: 'space-between', marginBottom: 8 }]}>
|
||||
<ApprovalStatusBadge status={item.status} />
|
||||
<Text style={[Styles.textSmallSemiBold, { color: colors.dimmed }]}>
|
||||
{item.createdAt}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Pengaju */}
|
||||
<View style={[Styles.rowItemsCenter, Styles.mb05]}>
|
||||
<MaterialCommunityIcons name="account-arrow-up-outline" size={15} color={colors.dimmed} style={{ marginRight: 6 }} />
|
||||
<Text style={[Styles.textMediumSemiBold, { color: colors.dimmed }]}>Diajukan Oleh: </Text>
|
||||
<Text style={[Styles.textMediumNormal]}>{item.submitter.name}</Text>
|
||||
</View>
|
||||
|
||||
{/* Approver */}
|
||||
<View style={[Styles.rowItemsCenter, item.note ? Styles.mb05 : {}]}>
|
||||
<MaterialCommunityIcons name="account-check-outline" size={15} color={colors.dimmed} style={{ marginRight: 6 }} />
|
||||
<Text style={[Styles.textMediumSemiBold, { color: colors.dimmed }]}>Disetujui Oleh: </Text>
|
||||
<Text style={[Styles.textMediumNormal]}>
|
||||
{item.approver?.name ?? '-'}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Catatan penolakan */}
|
||||
{item.note && (
|
||||
<View style={{
|
||||
backgroundColor: colors.error + '12',
|
||||
borderRadius: 8,
|
||||
padding: 8,
|
||||
marginTop: 4,
|
||||
}}>
|
||||
<Text style={[Styles.textSmallSemiBold, { color: colors.error, marginBottom: 2 }]}>
|
||||
Alasan Penolakan
|
||||
</Text>
|
||||
<Text style={[Styles.textMediumNormal, { color: colors.text }]}>
|
||||
{item.note}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
))
|
||||
) : (
|
||||
<Text style={[Styles.textDefault, { textAlign: 'center', color: colors.dimmed }]}>
|
||||
Belum ada riwayat persetujuan
|
||||
</Text>
|
||||
)}
|
||||
</ScrollView>
|
||||
</DrawerBottom>
|
||||
)
|
||||
}
|
||||
78
components/ModalTolakApproval.tsx
Normal file
78
components/ModalTolakApproval.tsx
Normal file
@@ -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 (
|
||||
<DrawerBottom
|
||||
isVisible={isVisible}
|
||||
setVisible={handleClose}
|
||||
title="Tolak Tugas"
|
||||
animation="slide"
|
||||
height={45}
|
||||
keyboard
|
||||
>
|
||||
<View style={{ flex: 1 }}>
|
||||
<InputForm
|
||||
label="Alasan Penolakan"
|
||||
placeholder="Tuliskan alasan penolakan..."
|
||||
type="default"
|
||||
multiline
|
||||
bg="transparent"
|
||||
value={note}
|
||||
onChange={setNote}
|
||||
error={error}
|
||||
errorText="Alasan penolakan wajib diisi"
|
||||
required
|
||||
/>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={handleSubmit}
|
||||
disabled={loading}
|
||||
style={{
|
||||
backgroundColor: loading ? colors.error + '60' : colors.error,
|
||||
borderRadius: 30,
|
||||
paddingVertical: 10,
|
||||
alignItems: 'center',
|
||||
marginTop: 8,
|
||||
}}
|
||||
>
|
||||
<Text style={[Styles.textDefaultSemiBold, Styles.cWhite]}>
|
||||
{loading ? 'Memproses...' : 'Tolak Tugas'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</DrawerBottom>
|
||||
)
|
||||
}
|
||||
@@ -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 ?
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<Pressable
|
||||
@@ -93,8 +101,8 @@ export default function ItemSectionTanggalTugas({ done, title, dateStart, dateEn
|
||||
}}
|
||||
>
|
||||
{/* Accent bar kiri */}
|
||||
{done !== undefined && (
|
||||
<View style={{ width: 4, backgroundColor: accentColor }} />
|
||||
{status !== undefined && (
|
||||
<View style={{ width: 4, backgroundColor: statusStyle.accent }} />
|
||||
)}
|
||||
|
||||
{/* Konten */}
|
||||
@@ -103,16 +111,16 @@ export default function ItemSectionTanggalTugas({ done, title, dateStart, dateEn
|
||||
{/* Judul + badge status */}
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 8 }}>
|
||||
<Text style={[Styles.textDefault, { flex: 1, marginRight: 8 }]}>{title}</Text>
|
||||
{done !== undefined && (
|
||||
{status !== undefined && (
|
||||
<View style={{
|
||||
backgroundColor: done ? successColor + '25' : dimmed + '18',
|
||||
backgroundColor: statusStyle.badge,
|
||||
borderRadius: 20,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 3,
|
||||
alignSelf: 'flex-start',
|
||||
}}>
|
||||
<Text style={[Styles.textSmallSemiBold, { color: done ? successColor : colors.dimmed }]}>
|
||||
{done ? 'Selesai' : 'Belum Selesai'}
|
||||
<Text style={[Styles.textSmallSemiBold, { color: statusStyle.text }]}>
|
||||
{statusStyle.label}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<ButtonMenuHeader onPress={() => { setVisible(true) }} />
|
||||
@@ -55,12 +77,10 @@ export default function HeaderRightMemberDetail({ active, id }: Props) {
|
||||
<View style={Styles.rowItemsCenter}>
|
||||
<MenuItemRow
|
||||
icon={<MaterialCommunityIcons name="toggle-switch-off-outline" color={colors.text} size={25} />}
|
||||
title={active ? "Non Aktifkan" : "Aktifkan"}
|
||||
title={active ? "Nonaktifkan" : "Aktifkan"}
|
||||
onPress={() => {
|
||||
setVisible(false)
|
||||
setTimeout(() => {
|
||||
setShowModal(true)
|
||||
}, 600)
|
||||
setTimeout(() => setShowModalActive(true), 600)
|
||||
}}
|
||||
/>
|
||||
<MenuItemRow
|
||||
@@ -71,18 +91,39 @@ export default function HeaderRightMemberDetail({ active, id }: Props) {
|
||||
router.push(`/member/edit/${id}`)
|
||||
}}
|
||||
/>
|
||||
{canManageApprover && (
|
||||
<MenuItemRow
|
||||
icon={<MaterialCommunityIcons name="shield-check-outline" color={colors.text} size={25} />}
|
||||
title={isApprover ? "Revoke Approver" : "Jadikan Approver"}
|
||||
color={colors.text}
|
||||
onPress={() => {
|
||||
setVisible(false)
|
||||
setTimeout(() => setShowModalApprover(true), 600)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</DrawerBottom>
|
||||
|
||||
<ModalConfirmation
|
||||
visible={showModal}
|
||||
visible={showModalActive}
|
||||
title="Konfirmasi"
|
||||
message={active ? 'Apakah anda yakin ingin menonaktifkan user?' : 'Apakah anda yakin ingin mengaktifkan user?'}
|
||||
message={active ? 'Apakah anda yakin ingin menonaktifkan anggota ini?' : 'Apakah anda yakin ingin mengaktifkan anggota ini?'}
|
||||
onConfirm={() => {
|
||||
setShowModal(false)
|
||||
setShowModalActive(false)
|
||||
handleActive()
|
||||
}}
|
||||
onCancel={() => setShowModal(false)}
|
||||
onCancel={() => setShowModalActive(false)}
|
||||
confirmText="Konfirmasi"
|
||||
cancelText="Batal"
|
||||
/>
|
||||
|
||||
<ModalConfirmation
|
||||
visible={showModalApprover}
|
||||
title="Konfirmasi"
|
||||
message={isApprover ? 'Apakah anda yakin ingin mencabut status approver user ini?' : 'Apakah anda yakin ingin menjadikan user ini sebagai approver?'}
|
||||
onConfirm={handleToggleApprover}
|
||||
onCancel={() => setShowModalApprover(false)}
|
||||
confirmText="Konfirmasi"
|
||||
cancelText="Batal"
|
||||
/>
|
||||
|
||||
@@ -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<Props[]>([]);
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [loadingAction, setLoadingAction] = useState(false)
|
||||
const [loadingRiwayat, setLoadingRiwayat] = useState(false)
|
||||
const [riwayatData, setRiwayatData] = useState<ApprovalRecord[]>([])
|
||||
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 (
|
||||
<>
|
||||
<View style={[Styles.mb15, Styles.mt10]}>
|
||||
<Text style={[Styles.textDefaultSemiBold, Styles.mv05]}>
|
||||
Tanggal & Tugas
|
||||
</Text>
|
||||
<Text style={[Styles.textDefaultSemiBold, Styles.mv05]}>Tanggal & Tugas</Text>
|
||||
<View>
|
||||
{
|
||||
loading ?
|
||||
arrSkeleton.map((item, index) => {
|
||||
return (
|
||||
<SkeletonTask key={index} />
|
||||
)
|
||||
})
|
||||
:
|
||||
data.length > 0
|
||||
?
|
||||
data.map((item, index) => {
|
||||
return (
|
||||
<ItemSectionTanggalTugas
|
||||
key={index}
|
||||
done={item.status === 1}
|
||||
title={item.title}
|
||||
dateStart={item.dateStart}
|
||||
dateEnd={item.dateEnd}
|
||||
files={item.files ?? []}
|
||||
onPress={() => {
|
||||
setTugas({ id: item.id, status: item.status })
|
||||
setModal(true)
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})
|
||||
:
|
||||
<Text style={[Styles.textDefault, { textAlign: 'center', color: colors.dimmed }]}>Tidak ada tugas</Text>
|
||||
{loading
|
||||
? arrSkeleton.map((_, index) => <SkeletonTask key={index} />)
|
||||
: data.length > 0
|
||||
? data.map((item, index) => (
|
||||
<ItemSectionTanggalTugas
|
||||
key={index}
|
||||
status={item.status}
|
||||
title={item.title}
|
||||
dateStart={item.dateStart}
|
||||
dateEnd={item.dateEnd}
|
||||
files={item.files ?? []}
|
||||
onPress={() => {
|
||||
setTugas({ id: item.id, status: item.status })
|
||||
setModal(true)
|
||||
}}
|
||||
/>
|
||||
))
|
||||
: <Text style={[Styles.textDefault, { textAlign: 'center', color: colors.dimmed }]}>Tidak ada tugas</Text>
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<DrawerBottom
|
||||
animation="slide"
|
||||
isVisible={isModal}
|
||||
setVisible={setModal}
|
||||
title="Menu"
|
||||
>
|
||||
<View style={Styles.rowItemsCenter}>
|
||||
{(member || (entityUser.role != "user" && entityUser.role != "coadmin")) && status != 3 && (
|
||||
<MenuItemRow
|
||||
icon={<MaterialCommunityIcons name="list-status" color={colors.text} size={25} />}
|
||||
title="Update Status"
|
||||
onPress={() => {
|
||||
setModal(false);
|
||||
setTimeout(() => setSelect(true), 600)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{/* Drawer menu */}
|
||||
<DrawerBottom animation="slide" isVisible={isModal} setVisible={setModal} title="Menu" height={40}>
|
||||
<View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
|
||||
|
||||
{/* Baris 1 — selalu tampil */}
|
||||
<MenuItemRow
|
||||
icon={<MaterialCommunityIcons name="file-multiple-outline" color={colors.text} size={25} />}
|
||||
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}`)
|
||||
}}
|
||||
/>
|
||||
<MenuItemRow
|
||||
icon={<MaterialCommunityIcons name="clock-time-three-outline" color={colors.text} size={25} />}
|
||||
title="Detail Waktu"
|
||||
onPress={() => {
|
||||
setModal(false);
|
||||
setModal(false)
|
||||
setTimeout(() => setModalDetail(true), 600)
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
{(member || (entityUser.role != "user" && entityUser.role != "coadmin")) && status != 3 && (
|
||||
<View style={[Styles.rowItemsCenter, Styles.mt15]}>
|
||||
<MenuItemRow
|
||||
icon={<MaterialCommunityIcons name="history" color={colors.text} size={25} />}
|
||||
title="Riwayat"
|
||||
onPress={() => {
|
||||
setModal(false)
|
||||
handleLoadRiwayat()
|
||||
setTimeout(() => setModalRiwayat(true), 600)
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Separator antar baris */}
|
||||
{(showAjukan || showApproverActions || (canTakeAction && isAdmin && status !== 3)) && (
|
||||
<View style={{ width: '100%', height: 15 }} />
|
||||
)}
|
||||
|
||||
{/* Baris 2 — semua aksi kondisional dalam satu baris */}
|
||||
{showAjukan && (
|
||||
<MenuItemRow
|
||||
icon={<MaterialCommunityIcons name="pencil-outline" color={colors.text} size={25} />}
|
||||
title="Edit Tugas"
|
||||
onPress={() => {
|
||||
setModal(false);
|
||||
router.push(`/project/update/${tugas.id}`);
|
||||
}}
|
||||
/>
|
||||
<MenuItemRow
|
||||
icon={<Ionicons name="trash-outline" color={colors.text} size={25} />}
|
||||
title="Hapus Tugas"
|
||||
icon={<MaterialCommunityIcons name="check-circle-outline" color={colors.text} size={25} />}
|
||||
title="Ajukan Selesai"
|
||||
disabled={loadingAction}
|
||||
onPress={() => {
|
||||
setModal(false)
|
||||
setTimeout(() => setShowDeleteModal(true), 600)
|
||||
setTimeout(() => handleSubmitAjukan(), 600)
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
)}
|
||||
{showApproverActions && (
|
||||
<MenuItemRow
|
||||
icon={<MaterialCommunityIcons name="shield-check-outline" color={colors.text} size={25} />}
|
||||
title="Persetujuan"
|
||||
disabled={loadingAction}
|
||||
onPress={() => {
|
||||
setModal(false)
|
||||
setTimeout(() => setModalPersetujuan(true), 600)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{canTakeAction && isAdmin && status !== 3 && (
|
||||
<>
|
||||
<MenuItemRow
|
||||
icon={<MaterialCommunityIcons name="pencil-outline" color={colors.text} size={25} />}
|
||||
title="Edit Tugas"
|
||||
onPress={() => {
|
||||
setModal(false)
|
||||
router.push(`/project/update/${tugas.id}`)
|
||||
}}
|
||||
/>
|
||||
<MenuItemRow
|
||||
icon={<Ionicons name="trash-outline" color={colors.text} size={25} />}
|
||||
title="Hapus Tugas"
|
||||
onPress={() => {
|
||||
setModal(false)
|
||||
setTimeout(() => setShowDeleteModal(true), 600)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</DrawerBottom>
|
||||
|
||||
{/* Drawer persetujuan */}
|
||||
<DrawerBottom animation="slide" isVisible={modalPersetujuan} setVisible={setModalPersetujuan} title="Persetujuan Tugas" height={25}>
|
||||
<View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
|
||||
<MenuItemRow
|
||||
icon={<MaterialCommunityIcons name="check-circle" color={colors.success} size={25} />}
|
||||
title="Setujui"
|
||||
color={colors.success}
|
||||
disabled={loadingAction}
|
||||
onPress={() => {
|
||||
setModalPersetujuan(false)
|
||||
setTimeout(() => setModalKonfirmasiSetujui(true), 600)
|
||||
}}
|
||||
/>
|
||||
<MenuItemRow
|
||||
icon={<MaterialCommunityIcons name="close-circle-outline" color={colors.error} size={25} />}
|
||||
title="Tolak"
|
||||
color={colors.error}
|
||||
disabled={loadingAction}
|
||||
onPress={() => {
|
||||
setModalPersetujuan(false)
|
||||
setTimeout(() => setModalTolak(true), 600)
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</DrawerBottom>
|
||||
|
||||
<ModalConfirmation
|
||||
visible={showDeleteModal}
|
||||
title="Konfirmasi"
|
||||
message="Apakah anda yakin ingin menghapus data ini?"
|
||||
onConfirm={() => {
|
||||
setShowDeleteModal(false)
|
||||
handleDelete()
|
||||
}}
|
||||
onConfirm={() => { setShowDeleteModal(false); handleDelete() }}
|
||||
onCancel={() => setShowDeleteModal(false)}
|
||||
confirmText="Hapus"
|
||||
cancelText="Batal"
|
||||
/>
|
||||
|
||||
<ModalSelect
|
||||
category="status-task"
|
||||
close={() => { setSelect(false) }}
|
||||
onSelect={(value) => {
|
||||
handleUpdate(Number(value.val))
|
||||
}}
|
||||
title="Status"
|
||||
open={isSelect}
|
||||
valChoose={String(tugas.status)}
|
||||
<ModalConfirmation
|
||||
visible={modalKonfirmasiSetujui}
|
||||
title="Konfirmasi"
|
||||
message="Apakah anda yakin ingin menyetujui tugas ini?"
|
||||
onConfirm={handleSetujui}
|
||||
onCancel={() => setModalKonfirmasiSetujui(false)}
|
||||
confirmText="Setujui"
|
||||
cancelText="Batal"
|
||||
/>
|
||||
|
||||
<ModalRiwayatApproval
|
||||
isVisible={modalRiwayat}
|
||||
setVisible={setModalRiwayat}
|
||||
data={riwayatData}
|
||||
loading={loadingRiwayat}
|
||||
/>
|
||||
|
||||
<ModalTolakApproval
|
||||
isVisible={modalTolak}
|
||||
setVisible={setModalTolak}
|
||||
onTolak={handleTolak}
|
||||
loading={loadingAction}
|
||||
/>
|
||||
|
||||
<ModalListDetailTugasProject
|
||||
|
||||
@@ -21,7 +21,7 @@ export default function SectionProgress({ progress, doneCount, totalCount }: Pro
|
||||
? 'Selesai'
|
||||
: progress > 0
|
||||
? 'Sedang berlangsung'
|
||||
: 'Belum dimulai';
|
||||
: 'Belum ada tugas yang diselesaikan';
|
||||
|
||||
useEffect(() => {
|
||||
animatedWidth.setValue(0);
|
||||
|
||||
@@ -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<ApprovalRecord[]>([])
|
||||
const { id, detail } = useLocalSearchParams<{ id: string, detail: string }>();
|
||||
const [data, setData] = useState<Props[]>([])
|
||||
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 (
|
||||
<>
|
||||
<View style={[Styles.mb15, Styles.mt10]}>
|
||||
<Text style={[Styles.textDefaultSemiBold, Styles.mv05]}>Tanggal & Tugas</Text>
|
||||
<View>
|
||||
{
|
||||
loading ?
|
||||
arrSkeleton.map((item, index) => {
|
||||
return (
|
||||
<SkeletonTask key={index} />
|
||||
)
|
||||
})
|
||||
:
|
||||
data.length > 0
|
||||
?
|
||||
data.map((item, index) => {
|
||||
return (
|
||||
<ItemSectionTanggalTugas
|
||||
key={index}
|
||||
done={item.status === 1}
|
||||
title={item.title}
|
||||
dateStart={item.dateStart}
|
||||
dateEnd={item.dateEnd}
|
||||
files={item.files ?? []}
|
||||
onPress={() => {
|
||||
setTugas({
|
||||
id: item.id,
|
||||
status: item.status
|
||||
})
|
||||
setModal(true)
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})
|
||||
:
|
||||
<Text style={[Styles.textDefault, { textAlign: 'center', color: colors.dimmed }]}>Tidak ada tugas</Text>
|
||||
{loading
|
||||
? arrSkeleton.map((_, index) => <SkeletonTask key={index} />)
|
||||
: data.length > 0
|
||||
? data.map((item, index) => (
|
||||
<ItemSectionTanggalTugas
|
||||
key={index}
|
||||
status={item.status}
|
||||
title={item.title}
|
||||
dateStart={item.dateStart}
|
||||
dateEnd={item.dateEnd}
|
||||
files={item.files ?? []}
|
||||
onPress={() => {
|
||||
setTugas({ id: item.id, status: item.status })
|
||||
setModal(true)
|
||||
}}
|
||||
/>
|
||||
))
|
||||
: <Text style={[Styles.textDefault, { textAlign: 'center', color: colors.dimmed }]}>Tidak ada tugas</Text>
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<DrawerBottom animation="slide" isVisible={isModal} setVisible={setModal} title="Menu">
|
||||
<View style={Styles.rowItemsCenter}>
|
||||
{((entityUser.role != "user" && entityUser.role != "coadmin") || isMemberDivision) && (
|
||||
<MenuItemRow
|
||||
icon={<MaterialCommunityIcons name="list-status" color={colors.text} size={25} />}
|
||||
title="Update Status"
|
||||
onPress={() => {
|
||||
setModal(false)
|
||||
setTimeout(() => setSelect(true), 600)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{/* Drawer menu */}
|
||||
<DrawerBottom animation="slide" isVisible={isModal} setVisible={setModal} title="Menu" height={40}>
|
||||
<View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
|
||||
|
||||
{/* Baris 1 — selalu tampil */}
|
||||
<MenuItemRow
|
||||
icon={<MaterialCommunityIcons name="file-multiple-outline" color={colors.text} size={25} />}
|
||||
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={<MaterialCommunityIcons name="clock-time-three-outline" color={colors.text} size={25} />}
|
||||
title="Detail Waktu"
|
||||
onPress={() => {
|
||||
setModal(false);
|
||||
setModal(false)
|
||||
setTimeout(() => setModalDetail(true), 600)
|
||||
}}
|
||||
/>
|
||||
<MenuItemRow
|
||||
icon={<MaterialCommunityIcons name="history" color={colors.text} size={25} />}
|
||||
title="Riwayat"
|
||||
onPress={() => {
|
||||
setModal(false)
|
||||
handleLoadRiwayat()
|
||||
setTimeout(() => setModalRiwayat(true), 600)
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Separator */}
|
||||
{(showAjukan || showApproverActions || (canTakeAction && isAdmin && status !== 3)) && (
|
||||
<View style={{ width: '100%', height: 15 }} />
|
||||
)}
|
||||
|
||||
{/* Baris 2 — aksi kondisional */}
|
||||
{showAjukan && (
|
||||
<MenuItemRow
|
||||
icon={<MaterialCommunityIcons name="check-circle-outline" color={colors.text} size={25} />}
|
||||
title="Ajukan Selesai"
|
||||
disabled={loadingAction}
|
||||
onPress={() => {
|
||||
setModal(false)
|
||||
setTimeout(() => handleSubmitAjukan(), 600)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showApproverActions && (
|
||||
<MenuItemRow
|
||||
icon={<MaterialCommunityIcons name="shield-check-outline" color={colors.text} size={25} />}
|
||||
title="Persetujuan"
|
||||
disabled={loadingAction}
|
||||
onPress={() => {
|
||||
setModal(false)
|
||||
setTimeout(() => setModalPersetujuan(true), 600)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{canTakeAction && isAdmin && status !== 3 && (
|
||||
<>
|
||||
<MenuItemRow
|
||||
icon={<MaterialCommunityIcons name="pencil-outline" color={colors.text} size={25} />}
|
||||
title="Edit Tugas"
|
||||
onPress={() => {
|
||||
setModal(false)
|
||||
router.push(`./update/${tugas.id}`)
|
||||
}}
|
||||
/>
|
||||
<MenuItemRow
|
||||
icon={<Ionicons name="trash-outline" color={colors.text} size={25} />}
|
||||
title="Hapus Tugas"
|
||||
onPress={() => {
|
||||
setModal(false)
|
||||
setTimeout(() => setShowDeleteModal(true), 600)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</DrawerBottom>
|
||||
|
||||
{/* Drawer persetujuan */}
|
||||
<DrawerBottom animation="slide" isVisible={modalPersetujuan} setVisible={setModalPersetujuan} title="Persetujuan Tugas" height={25}>
|
||||
<View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
|
||||
<MenuItemRow
|
||||
icon={<MaterialCommunityIcons name="check-circle" color={colors.success} size={25} />}
|
||||
title="Setujui"
|
||||
color={colors.success}
|
||||
disabled={loadingAction}
|
||||
onPress={() => {
|
||||
setModalPersetujuan(false)
|
||||
setTimeout(() => setModalKonfirmasiSetujui(true), 600)
|
||||
}}
|
||||
/>
|
||||
<MenuItemRow
|
||||
icon={<MaterialCommunityIcons name="close-circle-outline" color={colors.error} size={25} />}
|
||||
title="Tolak"
|
||||
color={colors.error}
|
||||
disabled={loadingAction}
|
||||
onPress={() => {
|
||||
setModalPersetujuan(false)
|
||||
setTimeout(() => setModalTolak(true), 600)
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
{((entityUser.role != "user" && entityUser.role != "coadmin") || isMemberDivision) && (
|
||||
<View style={[Styles.rowItemsCenter, Styles.mt15]}>
|
||||
<MenuItemRow
|
||||
icon={<MaterialCommunityIcons name="pencil-outline" color={colors.text} size={25} />}
|
||||
title="Edit Tugas"
|
||||
onPress={() => {
|
||||
setModal(false)
|
||||
router.push(`./update/${tugas.id}`)
|
||||
}}
|
||||
/>
|
||||
<MenuItemRow
|
||||
icon={<Ionicons name="trash-outline" color={colors.text} size={25} />}
|
||||
title="Hapus Tugas"
|
||||
onPress={() => {
|
||||
setModal(false)
|
||||
setTimeout(() => setShowDeleteModal(true), 600)
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</DrawerBottom>
|
||||
|
||||
<ModalConfirmation
|
||||
visible={showDeleteModal}
|
||||
title="Konfirmasi"
|
||||
message="Apakah anda yakin ingin menghapus data ini?"
|
||||
onConfirm={() => {
|
||||
setShowDeleteModal(false)
|
||||
handleDelete()
|
||||
}}
|
||||
onConfirm={() => { setShowDeleteModal(false); handleDelete() }}
|
||||
onCancel={() => setShowDeleteModal(false)}
|
||||
confirmText="Hapus"
|
||||
cancelText="Batal"
|
||||
/>
|
||||
|
||||
<ModalSelect
|
||||
category="status-task"
|
||||
close={() => setSelect(false)}
|
||||
onSelect={(value) => { handleUpdate(Number(value.val)) }}
|
||||
title="Status"
|
||||
open={isSelect}
|
||||
valChoose={String(tugas.status)}
|
||||
<ModalConfirmation
|
||||
visible={modalKonfirmasiSetujui}
|
||||
title="Konfirmasi"
|
||||
message="Apakah anda yakin ingin menyetujui tugas ini?"
|
||||
onConfirm={handleSetujui}
|
||||
onCancel={() => setModalKonfirmasiSetujui(false)}
|
||||
confirmText="Setujui"
|
||||
cancelText="Batal"
|
||||
/>
|
||||
|
||||
<ModalRiwayatApproval
|
||||
isVisible={modalRiwayat}
|
||||
setVisible={setModalRiwayat}
|
||||
data={riwayatData}
|
||||
loading={loadingRiwayat}
|
||||
/>
|
||||
|
||||
<ModalTolakApproval
|
||||
isVisible={modalTolak}
|
||||
setVisible={setModalTolak}
|
||||
onTolak={handleTolak}
|
||||
loading={loadingAction}
|
||||
/>
|
||||
|
||||
<ModalListDetailTugasTask
|
||||
@@ -243,4 +346,4 @@ export default function SectionTanggalTugasTask({ refreshing, isMemberDivision }
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
35
lib/api.ts
35
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;
|
||||
|
||||
Reference in New Issue
Block a user