Merge pull request 'amalia/07-mei-26' (#45) from amalia/07-mei-26 into join
Reviewed-on: #45
This commit is contained in:
@@ -154,7 +154,7 @@ export default function DetailTaskDivision() {
|
|||||||
}
|
}
|
||||||
<SectionProgress progress={progress} doneCount={taskStats?.done} totalCount={taskStats?.total} />
|
<SectionProgress progress={progress} doneCount={taskStats?.done} totalCount={taskStats?.total} />
|
||||||
<SectionReportTask refreshing={refreshing} />
|
<SectionReportTask refreshing={refreshing} />
|
||||||
<SectionTanggalTugasTask refreshing={refreshing} isMemberDivision={isMemberDivision} />
|
<SectionTanggalTugasTask refreshing={refreshing} isMemberDivision={isMemberDivision} isAdminDivision={isAdminDivision} status={data?.status} />
|
||||||
<SectionFileTask refreshing={refreshing} isMemberDivision={isMemberDivision} />
|
<SectionFileTask refreshing={refreshing} isMemberDivision={isMemberDivision} />
|
||||||
<SectionLinkTask refreshing={refreshing} isMemberDivision={isMemberDivision} />
|
<SectionLinkTask refreshing={refreshing} isMemberDivision={isMemberDivision} />
|
||||||
<SectionMemberTask refreshing={refreshing} isAdminDivision={isAdminDivision} />
|
<SectionMemberTask refreshing={refreshing} isAdminDivision={isAdminDivision} />
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { router, useLocalSearchParams } from "expo-router";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Pressable, RefreshControl, ScrollView, View, VirtualizedList } from "react-native";
|
import { Pressable, RefreshControl, ScrollView, View, VirtualizedList } from "react-native";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -36,6 +37,18 @@ export default function ListTask() {
|
|||||||
const { id, status, year } = useLocalSearchParams<{ id: string; status: string; year: string }>()
|
const { id, status, year } = useLocalSearchParams<{ id: string; status: string; year: string }>()
|
||||||
const [isList, setList] = useState(false)
|
const [isList, setList] = useState(false)
|
||||||
const { token, decryptToken } = useAuthSession()
|
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 [data, setData] = useState<Props[]>([])
|
||||||
const [search, setSearch] = useState("")
|
const [search, setSearch] = useState("")
|
||||||
const update = useSelector((state: any) => state.taskUpdate)
|
const update = useSelector((state: any) => state.taskUpdate)
|
||||||
@@ -172,13 +185,9 @@ export default function ListTask() {
|
|||||||
n={4}
|
n={4}
|
||||||
/>
|
/>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
<View style={[Styles.rowSpaceBetween]}>
|
<View style={[Styles.rowSpaceBetween, { alignItems: 'center' }]}>
|
||||||
<InputSearch width={68} onChange={setSearch} />
|
<InputSearch width={68} onChange={setSearch} />
|
||||||
<Pressable
|
<Pressable onPress={toggleView}>
|
||||||
onPress={() => {
|
|
||||||
setList(!isList);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
name={isList ? "format-list-bulleted" : "view-grid"}
|
name={isList ? "format-list-bulleted" : "view-grid"}
|
||||||
color={colors.text}
|
color={colors.text}
|
||||||
@@ -219,9 +228,10 @@ export default function ListTask() {
|
|||||||
router.push(`./task/${item.id}`);
|
router.push(`./task/${item.id}`);
|
||||||
}}
|
}}
|
||||||
borderType="bottom"
|
borderType="bottom"
|
||||||
|
bgColor="transparent"
|
||||||
icon={
|
icon={
|
||||||
<View style={[Styles.iconContent, ColorsStatus.lightGreen]}>
|
<View style={[Styles.iconContent]}>
|
||||||
<AntDesign name="areachart" size={25} color={"#384288"} />
|
<AntDesign name="areachart" size={25} color={"black"} />
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
title={item.title}
|
title={item.title}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
Ionicons,
|
Ionicons,
|
||||||
MaterialCommunityIcons
|
MaterialCommunityIcons
|
||||||
} from "@expo/vector-icons";
|
} from "@expo/vector-icons";
|
||||||
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||||
import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
|
import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { router, useLocalSearchParams } from "expo-router";
|
import { router, useLocalSearchParams } from "expo-router";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
@@ -37,6 +38,18 @@ export default function ListDivision() {
|
|||||||
cat?: string;
|
cat?: string;
|
||||||
}>();
|
}>();
|
||||||
const [isList, setList] = useState(false);
|
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 entityUser = useSelector((state: any) => state.user)
|
||||||
const { token, decryptToken } = useAuthSession()
|
const { token, decryptToken } = useAuthSession()
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
@@ -184,11 +197,7 @@ export default function ListDivision() {
|
|||||||
|
|
||||||
<View style={[Styles.rowSpaceBetween, { alignItems: 'center' }]}>
|
<View style={[Styles.rowSpaceBetween, { alignItems: 'center' }]}>
|
||||||
<InputSearch width={68} onChange={setSearch} />
|
<InputSearch width={68} onChange={setSearch} />
|
||||||
<Pressable
|
<Pressable onPress={toggleView}>
|
||||||
onPress={() => {
|
|
||||||
setList(!isList);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
name={isList ? "format-list-bulleted" : "view-grid"}
|
name={isList ? "format-list-bulleted" : "view-grid"}
|
||||||
color={colors.text}
|
color={colors.text}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import AppHeader from "@/components/AppHeader";
|
import AppHeader from "@/components/AppHeader";
|
||||||
import ImageUser from "@/components/imageNew";
|
import ImageUser from "@/components/imageNew";
|
||||||
import ItemDetailMember from "@/components/itemDetailMember";
|
|
||||||
import LabelStatus from "@/components/labelStatus";
|
import LabelStatus from "@/components/labelStatus";
|
||||||
import HeaderRightMemberDetail from "@/components/member/headerMemberDetail";
|
import HeaderRightMemberDetail from "@/components/member/headerMemberDetail";
|
||||||
import Skeleton from "@/components/skeleton";
|
import Skeleton from "@/components/skeleton";
|
||||||
@@ -11,6 +10,7 @@ import { valueRoleUser } from "@/constants/RoleUser";
|
|||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { apiGetProfile } from "@/lib/api";
|
import { apiGetProfile } from "@/lib/api";
|
||||||
import { useTheme } from "@/providers/ThemeProvider";
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
|
import { AntDesign, MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons";
|
||||||
import { LinearGradient } from "expo-linear-gradient";
|
import { LinearGradient } from "expo-linear-gradient";
|
||||||
import { router, Stack, useLocalSearchParams } from "expo-router";
|
import { router, Stack, useLocalSearchParams } from "expo-router";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
@@ -30,6 +30,7 @@ type Props = {
|
|||||||
group: string,
|
group: string,
|
||||||
img: string,
|
img: string,
|
||||||
isActive: boolean,
|
isActive: boolean,
|
||||||
|
isApprover: boolean,
|
||||||
role: string
|
role: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +90,7 @@ export default function MemberDetail() {
|
|||||||
showBack={true}
|
showBack={true}
|
||||||
onPressLeft={() => router.back()}
|
onPressLeft={() => router.back()}
|
||||||
right={
|
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]}
|
colors={[colors.header, colors.homeGradient]}
|
||||||
style={[Styles.wrapHeadViewMember]}
|
style={[Styles.wrapHeadViewMember]}
|
||||||
>
|
>
|
||||||
{
|
{loading ? (
|
||||||
loading ?
|
<>
|
||||||
<>
|
<Skeleton width={100} height={100} borderRadius={100} />
|
||||||
<Skeleton width={100} height={100} borderRadius={100} />
|
<Skeleton width={200} height={10} borderRadius={5} />
|
||||||
<Skeleton width={200} height={10} borderRadius={5} />
|
<Skeleton width={150} height={10} borderRadius={5} />
|
||||||
<Skeleton width={150} height={10} borderRadius={5} />
|
</>
|
||||||
</>
|
) : (
|
||||||
:
|
<>
|
||||||
<>
|
<Pressable onPress={() => setPreview(true)}>
|
||||||
<Pressable onPress={() => setPreview(true)}>
|
<View style={[Styles.memberAvatarRing]}>
|
||||||
<ImageUser src={`${ConstEnv.url_storage}/files/${data?.img}`} size="lg" onError={setErrorImg} />
|
<ImageUser src={`${ConstEnv.url_storage}/files/${data?.img}`} size="lg" onError={setErrorImg} />
|
||||||
</Pressable>
|
</View>
|
||||||
<Text style={[Styles.textSubtitle, Styles.cWhite, Styles.mt10, Styles.textCenter]}>{data?.name}</Text>
|
</Pressable>
|
||||||
<Text style={[Styles.textMediumNormal, Styles.cWhite]}>{data?.role}</Text>
|
<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>
|
</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>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import AppHeader from "@/components/AppHeader";
|
import AppHeader from "@/components/AppHeader";
|
||||||
import { ButtonHeader } from "@/components/buttonHeader";
|
import { ButtonHeader } from "@/components/buttonHeader";
|
||||||
import ItemDetailMember from "@/components/itemDetailMember";
|
|
||||||
import Text from "@/components/Text";
|
import Text from "@/components/Text";
|
||||||
import { assetUserImage } from "@/constants/AssetsError";
|
import { assetUserImage } from "@/constants/AssetsError";
|
||||||
import { ConstEnv } from "@/constants/ConstEnv";
|
import { ConstEnv } from "@/constants/ConstEnv";
|
||||||
@@ -9,7 +8,7 @@ import { apiGetProfile } from "@/lib/api";
|
|||||||
import { setEntities } from "@/lib/entitiesSlice";
|
import { setEntities } from "@/lib/entitiesSlice";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
import { useTheme } from "@/providers/ThemeProvider";
|
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 { LinearGradient } from "expo-linear-gradient";
|
||||||
import { router, Stack } from "expo-router";
|
import { router, Stack } from "expo-router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
@@ -42,6 +41,15 @@ export default function Profile() {
|
|||||||
setRefreshing(false)
|
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 (
|
return (
|
||||||
<SafeAreaView style={[Styles.flex1, { backgroundColor: colors.background }]}>
|
<SafeAreaView style={[Styles.flex1, { backgroundColor: colors.background }]}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
@@ -56,9 +64,7 @@ export default function Profile() {
|
|||||||
right={
|
right={
|
||||||
<ButtonHeader
|
<ButtonHeader
|
||||||
item={<Feather name="settings" size={20} color="white" />}
|
item={<Feather name="settings" size={20} color="white" />}
|
||||||
onPress={() => {
|
onPress={() => router.push('/setting')}
|
||||||
router.push('/setting')
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -75,32 +81,47 @@ export default function Profile() {
|
|||||||
}
|
}
|
||||||
style={[Styles.h100, { backgroundColor: colors.background }]}
|
style={[Styles.h100, { backgroundColor: colors.background }]}
|
||||||
>
|
>
|
||||||
<View style={[Styles.flexColumn]}>
|
<LinearGradient
|
||||||
<LinearGradient
|
colors={[colors.header, colors.homeGradient]}
|
||||||
colors={[colors.header, colors.homeGradient]}
|
style={[Styles.wrapHeadViewMember]}
|
||||||
style={[Styles.wrapHeadViewMember]}
|
>
|
||||||
>
|
<Pressable onPress={() => setPreview(true)}>
|
||||||
<Pressable onPress={() => setPreview(true)}>
|
<View style={[Styles.memberAvatarRing]}>
|
||||||
<Image
|
<Image
|
||||||
source={error ? require("../../assets/images/user.jpg") : { uri: `${ConstEnv.url_storage}/files/${entities.img}` }}
|
source={error ? require("../../assets/images/user.jpg") : { uri: `${ConstEnv.url_storage}/files/${entities.img}` }}
|
||||||
onError={() => { setError(true) }}
|
onError={() => setError(true)}
|
||||||
style={[Styles.userProfileBig]}
|
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>
|
</View>
|
||||||
{/* Note: ItemDetailMember might need updates to support dynamic colors if it uses default text colors */}
|
</Pressable>
|
||||||
<ItemDetailMember category="nik" value={entities.nik} />
|
<Text style={[Styles.textSubtitle, Styles.cWhite, Styles.mt10, Styles.textCenter]}>{entities.name}</Text>
|
||||||
<ItemDetailMember category="group" value={entities.group} />
|
<Text style={[Styles.textMediumNormal, Styles.cWhiteDimmed]}>{entities.role}</Text>
|
||||||
<ItemDetailMember category="position" value={entities.position} />
|
{entities.isApprover && (
|
||||||
<ItemDetailMember category="phone" value={`0${entities.phone}`} />
|
<View style={[Styles.memberBadgeRow, { justifyContent: 'center' }]}>
|
||||||
<ItemDetailMember category="email" value={entities.email} />
|
<View style={[Styles.memberBadgeApprover]}>
|
||||||
<ItemDetailMember category="gender" value={entities.gender == "F" ? 'Perempuan' : 'Laki-laki'} />
|
<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>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { router, useLocalSearchParams } from "expo-router";
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { Pressable, RefreshControl, ScrollView, View, VirtualizedList } from "react-native";
|
import { Pressable, RefreshControl, ScrollView, View, VirtualizedList } from "react-native";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -50,6 +51,18 @@ export default function ListProject() {
|
|||||||
const [search, setSearch] = useState("")
|
const [search, setSearch] = useState("")
|
||||||
const [isList, setList] = useState(false)
|
const [isList, setList] = useState(false)
|
||||||
const update = useSelector((state: any) => state.projectUpdate)
|
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 queryClient = useQueryClient()
|
||||||
const [refreshing, setRefreshing] = useState(false)
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
|
|
||||||
@@ -188,11 +201,7 @@ export default function ListProject() {
|
|||||||
|
|
||||||
<View style={[Styles.rowSpaceBetween, Styles.rowItemsCenter]}>
|
<View style={[Styles.rowSpaceBetween, Styles.rowItemsCenter]}>
|
||||||
<InputSearch width={68} onChange={setSearch} />
|
<InputSearch width={68} onChange={setSearch} />
|
||||||
<Pressable
|
<Pressable onPress={toggleView}>
|
||||||
onPress={() => {
|
|
||||||
setList(!isList);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
name={isList ? "format-list-bulleted" : "view-grid"}
|
name={isList ? "format-list-bulleted" : "view-grid"}
|
||||||
color={colors.text}
|
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
|
height?: number
|
||||||
backdropPressable?: boolean
|
backdropPressable?: boolean
|
||||||
keyboard?: 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 tinggiScreen = Dimensions.get("window").height;
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const tinggiInput = height != undefined ? height : 25
|
const tinggiInput = height != undefined ? height : 25
|
||||||
@@ -38,6 +40,9 @@ export default function DrawerBottom({ isVisible, setVisible, title, children, a
|
|||||||
backdropTransitionOutTiming={500}
|
backdropTransitionOutTiming={500}
|
||||||
useNativeDriverForBackdrop={true}
|
useNativeDriverForBackdrop={true}
|
||||||
propagateSwipe={true}
|
propagateSwipe={true}
|
||||||
|
scrollTo={scrollTo}
|
||||||
|
scrollOffset={scrollOffset}
|
||||||
|
scrollOffsetMax={200}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
keyboard ?
|
keyboard ?
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export default function CaraouselHome({ refreshing }: { refreshing: boolean }) {
|
|||||||
async function handleUser() {
|
async function handleUser() {
|
||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
const response = await apiGetProfile({ id: hasil })
|
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(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export default function CaraouselHome2({ refreshing }: { refreshing: boolean })
|
|||||||
// Sync User Role to Redux
|
// Sync User Role to Redux
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (profile) {
|
if (profile) {
|
||||||
dispatch(setEntityUser({ role: profile.idUserRole, admin: false }))
|
dispatch(setEntityUser({ role: profile.idUserRole, admin: false, isApprover: profile.isApprover ?? false }))
|
||||||
}
|
}
|
||||||
}, [profile, dispatch])
|
}, [profile, dispatch])
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ type FileItem = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
done?: boolean
|
status?: number // 0=belum selesai, 1=selesai, 2=menunggu persetujuan
|
||||||
title: string
|
title: string
|
||||||
dateStart: string
|
dateStart: string
|
||||||
dateEnd: string
|
dateEnd: string
|
||||||
@@ -64,7 +64,15 @@ function getFileIcon(extension: string): keyof typeof MaterialCommunityIcons.gly
|
|||||||
return 'file-outline'
|
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 { colors, activeTheme } = useTheme()
|
||||||
const [containerWidth, setContainerWidth] = useState(0)
|
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 dimmed = colors.dimmed.slice(0, 7)
|
||||||
const successColor = activeTheme === 'dark' ? '#51CF66' : colors.success
|
const successColor = activeTheme === 'dark' ? '#51CF66' : colors.success
|
||||||
const accentColor = done === true ? successColor : dimmed + '80'
|
const statusStyle = getStatusStyle(status, successColor, dimmed)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
@@ -93,8 +101,8 @@ export default function ItemSectionTanggalTugas({ done, title, dateStart, dateEn
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Accent bar kiri */}
|
{/* Accent bar kiri */}
|
||||||
{done !== undefined && (
|
{status !== undefined && (
|
||||||
<View style={{ width: 4, backgroundColor: accentColor }} />
|
<View style={{ width: 4, backgroundColor: statusStyle.accent }} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Konten */}
|
{/* Konten */}
|
||||||
@@ -103,16 +111,16 @@ export default function ItemSectionTanggalTugas({ done, title, dateStart, dateEn
|
|||||||
{/* Judul + badge status */}
|
{/* Judul + badge status */}
|
||||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 8 }}>
|
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 8 }}>
|
||||||
<Text style={[Styles.textDefault, { flex: 1, marginRight: 8 }]}>{title}</Text>
|
<Text style={[Styles.textDefault, { flex: 1, marginRight: 8 }]}>{title}</Text>
|
||||||
{done !== undefined && (
|
{status !== undefined && (
|
||||||
<View style={{
|
<View style={{
|
||||||
backgroundColor: done ? successColor + '25' : dimmed + '18',
|
backgroundColor: statusStyle.badge,
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
paddingHorizontal: 8,
|
paddingHorizontal: 8,
|
||||||
paddingVertical: 3,
|
paddingVertical: 3,
|
||||||
alignSelf: 'flex-start',
|
alignSelf: 'flex-start',
|
||||||
}}>
|
}}>
|
||||||
<Text style={[Styles.textSmallSemiBold, { color: done ? successColor : colors.dimmed }]}>
|
<Text style={[Styles.textSmallSemiBold, { color: statusStyle.text }]}>
|
||||||
{done ? 'Selesai' : 'Belum Selesai'}
|
{statusStyle.label}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import Styles from "@/constants/Styles"
|
import Styles from "@/constants/Styles"
|
||||||
import { apiDeleteUser } from "@/lib/api"
|
import { apiDeleteUser, apiToggleApprover } from "@/lib/api"
|
||||||
import { setUpdateMember } from "@/lib/memberSlice"
|
import { setUpdateMember } from "@/lib/memberSlice"
|
||||||
import { useAuthSession } from "@/providers/AuthProvider"
|
import { useAuthSession } from "@/providers/AuthProvider"
|
||||||
import { useTheme } from "@/providers/ThemeProvider"
|
import { useTheme } from "@/providers/ThemeProvider"
|
||||||
@@ -16,14 +16,17 @@ import MenuItemRow from "../menuItemRow"
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
active: any,
|
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 { token, decryptToken } = useAuthSession()
|
||||||
const [isVisible, setVisible] = useState(false)
|
const [isVisible, setVisible] = useState(false)
|
||||||
const update = useSelector((state: any) => state.memberUpdate)
|
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 { colors } = useTheme();
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
|
|
||||||
@@ -37,17 +40,36 @@ export default function HeaderRightMemberDetail({ active, id }: Props) {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error : any ) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
const message = error?.response?.data?.message || "Gagal mengupdate data"
|
const message = error?.response?.data?.message || "Gagal mengupdate data"
|
||||||
|
|
||||||
Toast.show({ type: 'small', text1: message })
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setVisible(false)
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<ButtonMenuHeader onPress={() => { setVisible(true) }} />
|
<ButtonMenuHeader onPress={() => { setVisible(true) }} />
|
||||||
@@ -55,12 +77,10 @@ export default function HeaderRightMemberDetail({ active, id }: Props) {
|
|||||||
<View style={Styles.rowItemsCenter}>
|
<View style={Styles.rowItemsCenter}>
|
||||||
<MenuItemRow
|
<MenuItemRow
|
||||||
icon={<MaterialCommunityIcons name="toggle-switch-off-outline" color={colors.text} size={25} />}
|
icon={<MaterialCommunityIcons name="toggle-switch-off-outline" color={colors.text} size={25} />}
|
||||||
title={active ? "Non Aktifkan" : "Aktifkan"}
|
title={active ? "Nonaktifkan" : "Aktifkan"}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setVisible(false)
|
setVisible(false)
|
||||||
setTimeout(() => {
|
setTimeout(() => setShowModalActive(true), 600)
|
||||||
setShowModal(true)
|
|
||||||
}, 600)
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<MenuItemRow
|
<MenuItemRow
|
||||||
@@ -71,18 +91,39 @@ export default function HeaderRightMemberDetail({ active, id }: Props) {
|
|||||||
router.push(`/member/edit/${id}`)
|
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>
|
</View>
|
||||||
</DrawerBottom>
|
</DrawerBottom>
|
||||||
|
|
||||||
<ModalConfirmation
|
<ModalConfirmation
|
||||||
visible={showModal}
|
visible={showModalActive}
|
||||||
title="Konfirmasi"
|
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={() => {
|
onConfirm={() => {
|
||||||
setShowModal(false)
|
setShowModalActive(false)
|
||||||
handleActive()
|
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"
|
confirmText="Konfirmasi"
|
||||||
cancelText="Batal"
|
cancelText="Batal"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import Styles from "@/constants/Styles";
|
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 { setUpdateProject } from "@/lib/projectUpdate";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
import { useTheme } from "@/providers/ThemeProvider";
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
@@ -13,7 +13,8 @@ import DrawerBottom from "../drawerBottom";
|
|||||||
import ItemSectionTanggalTugas from "../itemSectionTanggalTugas";
|
import ItemSectionTanggalTugas from "../itemSectionTanggalTugas";
|
||||||
import MenuItemRow from "../menuItemRow";
|
import MenuItemRow from "../menuItemRow";
|
||||||
import ModalConfirmation from "../ModalConfirmation";
|
import ModalConfirmation from "../ModalConfirmation";
|
||||||
import ModalSelect from "../modalSelect";
|
import ModalRiwayatApproval from "../ModalRiwayatApproval";
|
||||||
|
import ModalTolakApproval from "../ModalTolakApproval";
|
||||||
import SkeletonTask from "../skeletonTask";
|
import SkeletonTask from "../skeletonTask";
|
||||||
import Text from "../Text";
|
import Text from "../Text";
|
||||||
import ModalListDetailTugasProject from "./modalListDetailTugasProject";
|
import ModalListDetailTugasProject from "./modalListDetailTugasProject";
|
||||||
@@ -22,41 +23,52 @@ type Props = {
|
|||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
desc: string;
|
desc: string;
|
||||||
status: 1;
|
status: number;
|
||||||
dateStart: string;
|
dateStart: string;
|
||||||
dateEnd: string;
|
dateEnd: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
files?: { name: string; extension: 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 }) {
|
export default function SectionTanggalTugasProject({ status, member, refreshing }: { status: number | undefined, member: boolean, refreshing?: boolean }) {
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const entityUser = useSelector((state: any) => state.user)
|
const entityUser = useSelector((state: any) => state.user)
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const update = useSelector((state: any) => state.projectUpdate)
|
const update = useSelector((state: any) => state.projectUpdate)
|
||||||
const [isModal, setModal] = useState(false);
|
const [isModal, setModal] = useState(false);
|
||||||
const [isSelect, setSelect] = useState(false);
|
|
||||||
const { token, decryptToken } = useAuthSession();
|
const { token, decryptToken } = useAuthSession();
|
||||||
const [modalDetail, setModalDetail] = useState(false)
|
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 { id } = useLocalSearchParams<{ id: string }>();
|
||||||
const [data, setData] = useState<Props[]>([]);
|
const [data, setData] = useState<Props[]>([]);
|
||||||
const [loading, setLoading] = useState(true)
|
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 arrSkeleton = Array.from({ length: 5 });
|
||||||
const [tugas, setTugas] = useState({
|
const [tugas, setTugas] = useState({ id: '', status: 0 })
|
||||||
id: '',
|
|
||||||
status: 0,
|
|
||||||
})
|
|
||||||
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
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) {
|
async function handleLoad(loading: boolean) {
|
||||||
try {
|
try {
|
||||||
setLoading(loading)
|
setLoading(loading)
|
||||||
const hasil = await decryptToken(String(token?.current));
|
const hasil = await decryptToken(String(token?.current));
|
||||||
const response = await apiGetProjectOne({
|
const response = await apiGetProjectOne({ user: hasil, cat: "task", id: id });
|
||||||
user: hasil,
|
|
||||||
cat: "task",
|
|
||||||
id: id,
|
|
||||||
});
|
|
||||||
setData(response.data);
|
setData(response.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@@ -65,178 +77,268 @@ export default function SectionTanggalTugasProject({ status, member, refreshing
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => { handleLoad(false) }, [update.task]);
|
||||||
handleLoad(false);
|
useEffect(() => { if (refreshing) handleLoad(false) }, [refreshing]);
|
||||||
}, [update.task]);
|
useEffect(() => { handleLoad(true) }, []);
|
||||||
|
|
||||||
useEffect(() => {
|
async function handleLoadRiwayat() {
|
||||||
if (refreshing)
|
|
||||||
handleLoad(false);
|
|
||||||
}, [refreshing]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
handleLoad(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
|
|
||||||
async function handleUpdate(status: number) {
|
|
||||||
try {
|
try {
|
||||||
const hasil = await decryptToken(String(token?.current));
|
setLoadingRiwayat(true)
|
||||||
const response = await apiUpdateStatusProjectTask({
|
const hasil = await decryptToken(String(token?.current))
|
||||||
user: hasil,
|
const response = await apiGetProjectTaskApprovals({ user: hasil, id: tugas.id })
|
||||||
idProject: id,
|
setRiwayatData(response.data ?? [])
|
||||||
status: status,
|
} catch (error) {
|
||||||
}, tugas.id);
|
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) {
|
if (response.success) {
|
||||||
dispatch(setUpdateProject({ ...update, progress: !update.progress, task: !update.task }))
|
dispatch(setUpdateProject({ ...update, progress: !update.progress, task: !update.task }))
|
||||||
setSelect(false);
|
Toast.show({ type: 'small', text1: 'Berhasil mengajukan task untuk persetujuan' })
|
||||||
Toast.show({ type: 'small', text1: 'Berhasil mengubah data', })
|
} else {
|
||||||
|
Toast.show({ type: 'small', text1: response.message })
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
const message = error?.response?.data?.message || "Gagal mengajukan persetujuan"
|
||||||
const message = error?.response?.data?.message || "Gagal mengubah data"
|
|
||||||
|
|
||||||
Toast.show({ type: 'small', text1: message })
|
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() {
|
async function handleDelete() {
|
||||||
try {
|
try {
|
||||||
const hasil = await decryptToken(String(token?.current));
|
const hasil = await decryptToken(String(token?.current));
|
||||||
const response = await apiDeleteProjectTask({
|
const response = await apiDeleteProjectTask({ user: hasil, idProject: id }, tugas.id);
|
||||||
user: hasil,
|
|
||||||
idProject: id,
|
|
||||||
}, tugas.id);
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
dispatch(setUpdateProject({ ...update, progress: !update.progress, task: !update.task }))
|
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) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
|
||||||
const message = error?.response?.data?.message || "Gagal menghapus data"
|
const message = error?.response?.data?.message || "Gagal menghapus data"
|
||||||
|
|
||||||
Toast.show({ type: 'small', text1: message })
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<View style={[Styles.mb15, Styles.mt10]}>
|
<View style={[Styles.mb15, Styles.mt10]}>
|
||||||
<Text style={[Styles.textDefaultSemiBold, Styles.mv05]}>
|
<Text style={[Styles.textDefaultSemiBold, Styles.mv05]}>Tanggal & Tugas</Text>
|
||||||
Tanggal & Tugas
|
|
||||||
</Text>
|
|
||||||
<View>
|
<View>
|
||||||
{
|
{loading
|
||||||
loading ?
|
? arrSkeleton.map((_, index) => <SkeletonTask key={index} />)
|
||||||
arrSkeleton.map((item, index) => {
|
: data.length > 0
|
||||||
return (
|
? data.map((item, index) => (
|
||||||
<SkeletonTask key={index} />
|
<ItemSectionTanggalTugas
|
||||||
)
|
key={index}
|
||||||
})
|
status={item.status}
|
||||||
:
|
title={item.title}
|
||||||
data.length > 0
|
dateStart={item.dateStart}
|
||||||
?
|
dateEnd={item.dateEnd}
|
||||||
data.map((item, index) => {
|
files={item.files ?? []}
|
||||||
return (
|
onPress={() => {
|
||||||
<ItemSectionTanggalTugas
|
setTugas({ id: item.id, status: item.status })
|
||||||
key={index}
|
setModal(true)
|
||||||
done={item.status === 1}
|
}}
|
||||||
title={item.title}
|
/>
|
||||||
dateStart={item.dateStart}
|
))
|
||||||
dateEnd={item.dateEnd}
|
: <Text style={[Styles.textDefault, { textAlign: 'center', color: colors.dimmed }]}>Tidak ada tugas</Text>
|
||||||
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>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<DrawerBottom
|
{/* Drawer menu */}
|
||||||
animation="slide"
|
<DrawerBottom animation="slide" isVisible={isModal} setVisible={setModal} title="Menu" height={40}>
|
||||||
isVisible={isModal}
|
<View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
|
||||||
setVisible={setModal}
|
|
||||||
title="Menu"
|
{/* Baris 1 — selalu tampil */}
|
||||||
>
|
|
||||||
<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)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<MenuItemRow
|
<MenuItemRow
|
||||||
icon={<MaterialCommunityIcons name="file-multiple-outline" color={colors.text} size={25} />}
|
icon={<MaterialCommunityIcons name="file-multiple-outline" color={colors.text} size={25} />}
|
||||||
title="File Tugas"
|
title="File Tugas"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setModal(false);
|
setModal(false)
|
||||||
router.push(`/project/${id}/tugas-file/${tugas.id}?member=${member}`);
|
router.push(`/project/${id}/tugas-file/${tugas.id}?member=${member}`)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<MenuItemRow
|
<MenuItemRow
|
||||||
icon={<MaterialCommunityIcons name="clock-time-three-outline" color={colors.text} size={25} />}
|
icon={<MaterialCommunityIcons name="clock-time-three-outline" color={colors.text} size={25} />}
|
||||||
title="Detail Waktu"
|
title="Detail Waktu"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setModal(false);
|
setModal(false)
|
||||||
setTimeout(() => setModalDetail(true), 600)
|
setTimeout(() => setModalDetail(true), 600)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</View>
|
<MenuItemRow
|
||||||
{(member || (entityUser.role != "user" && entityUser.role != "coadmin")) && status != 3 && (
|
icon={<MaterialCommunityIcons name="history" color={colors.text} size={25} />}
|
||||||
<View style={[Styles.rowItemsCenter, Styles.mt15]}>
|
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
|
<MenuItemRow
|
||||||
icon={<MaterialCommunityIcons name="pencil-outline" color={colors.text} size={25} />}
|
icon={<MaterialCommunityIcons name="check-circle-outline" color={colors.text} size={25} />}
|
||||||
title="Edit Tugas"
|
title="Ajukan Selesai"
|
||||||
onPress={() => {
|
disabled={loadingAction}
|
||||||
setModal(false);
|
|
||||||
router.push(`/project/update/${tugas.id}`);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<MenuItemRow
|
|
||||||
icon={<Ionicons name="trash-outline" color={colors.text} size={25} />}
|
|
||||||
title="Hapus Tugas"
|
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setModal(false)
|
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>
|
</DrawerBottom>
|
||||||
|
|
||||||
<ModalConfirmation
|
<ModalConfirmation
|
||||||
visible={showDeleteModal}
|
visible={showDeleteModal}
|
||||||
title="Konfirmasi"
|
title="Konfirmasi"
|
||||||
message="Apakah anda yakin ingin menghapus data ini?"
|
message="Apakah anda yakin ingin menghapus data ini?"
|
||||||
onConfirm={() => {
|
onConfirm={() => { setShowDeleteModal(false); handleDelete() }}
|
||||||
setShowDeleteModal(false)
|
|
||||||
handleDelete()
|
|
||||||
}}
|
|
||||||
onCancel={() => setShowDeleteModal(false)}
|
onCancel={() => setShowDeleteModal(false)}
|
||||||
confirmText="Hapus"
|
confirmText="Hapus"
|
||||||
cancelText="Batal"
|
cancelText="Batal"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ModalSelect
|
<ModalConfirmation
|
||||||
category="status-task"
|
visible={modalKonfirmasiSetujui}
|
||||||
close={() => { setSelect(false) }}
|
title="Konfirmasi"
|
||||||
onSelect={(value) => {
|
message="Apakah anda yakin ingin menyetujui tugas ini?"
|
||||||
handleUpdate(Number(value.val))
|
onConfirm={handleSetujui}
|
||||||
}}
|
onCancel={() => setModalKonfirmasiSetujui(false)}
|
||||||
title="Status"
|
confirmText="Setujui"
|
||||||
open={isSelect}
|
cancelText="Batal"
|
||||||
valChoose={String(tugas.status)}
|
/>
|
||||||
|
|
||||||
|
<ModalRiwayatApproval
|
||||||
|
isVisible={modalRiwayat}
|
||||||
|
setVisible={setModalRiwayat}
|
||||||
|
data={riwayatData}
|
||||||
|
loading={loadingRiwayat}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ModalTolakApproval
|
||||||
|
isVisible={modalTolak}
|
||||||
|
setVisible={setModalTolak}
|
||||||
|
onTolak={handleTolak}
|
||||||
|
loading={loadingAction}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ModalListDetailTugasProject
|
<ModalListDetailTugasProject
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export default function SectionProgress({ progress, doneCount, totalCount }: Pro
|
|||||||
? 'Selesai'
|
? 'Selesai'
|
||||||
: progress > 0
|
: progress > 0
|
||||||
? 'Sedang berlangsung'
|
? 'Sedang berlangsung'
|
||||||
: 'Belum dimulai';
|
: 'Belum ada tugas yang diselesaikan';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
animatedWidth.setValue(0);
|
animatedWidth.setValue(0);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import Styles from "@/constants/Styles";
|
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 { setUpdateTask } from "@/lib/taskUpdate";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
import { useTheme } from "@/providers/ThemeProvider";
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
@@ -13,7 +13,8 @@ import DrawerBottom from "../drawerBottom";
|
|||||||
import ItemSectionTanggalTugas from "../itemSectionTanggalTugas";
|
import ItemSectionTanggalTugas from "../itemSectionTanggalTugas";
|
||||||
import MenuItemRow from "../menuItemRow";
|
import MenuItemRow from "../menuItemRow";
|
||||||
import ModalConfirmation from "../ModalConfirmation";
|
import ModalConfirmation from "../ModalConfirmation";
|
||||||
import ModalSelect from "../modalSelect";
|
import ModalRiwayatApproval from "../ModalRiwayatApproval";
|
||||||
|
import ModalTolakApproval from "../ModalTolakApproval";
|
||||||
import SkeletonTask from "../skeletonTask";
|
import SkeletonTask from "../skeletonTask";
|
||||||
import Text from "../Text";
|
import Text from "../Text";
|
||||||
import ModalListDetailTugasTask from "./modalListDetailTugasTask";
|
import ModalListDetailTugasTask from "./modalListDetailTugasTask";
|
||||||
@@ -28,25 +29,41 @@ type Props = {
|
|||||||
files?: { name: string; extension: string }[];
|
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 { colors } = useTheme()
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const entityUser = useSelector((state: any) => state.user);
|
const entityUser = useSelector((state: any) => state.user);
|
||||||
const update = useSelector((state: any) => state.taskUpdate)
|
const update = useSelector((state: any) => state.taskUpdate)
|
||||||
const [isModal, setModal] = useState(false)
|
const [isModal, setModal] = useState(false)
|
||||||
const [isSelect, setSelect] = useState(false)
|
|
||||||
const { token, decryptToken } = useAuthSession()
|
const { token, decryptToken } = useAuthSession()
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [loadingAction, setLoadingAction] = useState(false)
|
||||||
|
const [loadingRiwayat, setLoadingRiwayat] = useState(false)
|
||||||
const arrSkeleton = Array.from({ length: 5 })
|
const arrSkeleton = Array.from({ length: 5 })
|
||||||
const [modalDetail, setModalDetail] = useState(false)
|
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 { id, detail } = useLocalSearchParams<{ id: string, detail: string }>();
|
||||||
const [data, setData] = useState<Props[]>([])
|
const [data, setData] = useState<Props[]>([])
|
||||||
const [tugas, setTugas] = useState({
|
const [tugas, setTugas] = useState({ id: '', status: 0 })
|
||||||
id: '',
|
|
||||||
status: 0,
|
|
||||||
})
|
|
||||||
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
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) {
|
async function handleLoad(loading: boolean) {
|
||||||
try {
|
try {
|
||||||
setLoading(loading)
|
setLoading(loading)
|
||||||
@@ -60,126 +77,136 @@ export default function SectionTanggalTugasTask({ refreshing, isMemberDivision }
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleUpdate(status: number) {
|
useEffect(() => { handleLoad(false) }, [update.task])
|
||||||
try {
|
useEffect(() => { if (refreshing) handleLoad(false) }, [refreshing])
|
||||||
const hasil = await decryptToken(String(token?.current));
|
useEffect(() => { handleLoad(true) }, [])
|
||||||
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"
|
|
||||||
|
|
||||||
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 {
|
} finally {
|
||||||
setSelect(false)
|
setLoadingRiwayat(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
async function handleSubmitAjukan() {
|
||||||
handleLoad(false)
|
try {
|
||||||
}, [update.task])
|
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(() => {
|
async function handleSetujui() {
|
||||||
if (refreshing)
|
try {
|
||||||
handleLoad(false);
|
setLoadingAction(true)
|
||||||
}, [refreshing]);
|
const hasil = await decryptToken(String(token?.current))
|
||||||
|
const response = await apiApproveRejectTaskTugas({ user: hasil, id: tugas.id, action: 'approve' })
|
||||||
|
if (response.success) {
|
||||||
useEffect(() => {
|
dispatch(setUpdateTask({ ...update, progress: !update.progress, task: !update.task }))
|
||||||
handleLoad(true)
|
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() {
|
async function handleDelete() {
|
||||||
try {
|
try {
|
||||||
const hasil = await decryptToken(String(token?.current));
|
const hasil = await decryptToken(String(token?.current));
|
||||||
const response = await apiDeleteTaskTugas({
|
const response = await apiDeleteTaskTugas({ user: hasil, idProject: detail }, tugas.id);
|
||||||
user: hasil,
|
|
||||||
idProject: detail,
|
|
||||||
}, tugas.id);
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
dispatch(setUpdateTask({ ...update, progress: !update.progress, task: !update.task }))
|
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 {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message })
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
Toast.show({ type: 'small', text1: error?.response?.data?.message || "Gagal menghapus data" })
|
||||||
const message = error?.response?.data?.message || "Gagal menghapus data"
|
|
||||||
|
|
||||||
Toast.show({ type: 'small', text1: message })
|
|
||||||
} finally {
|
|
||||||
setModal(false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const canApprove = isApprover || isAdminDivision
|
||||||
|
const showAjukan = (isMemberDivision || canApprove) && tugas.status === 0 && status !== 3
|
||||||
|
const showApproverActions = canApprove && tugas.status === 2
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<View style={[Styles.mb15, Styles.mt10]}>
|
<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>
|
<View>
|
||||||
{
|
{loading
|
||||||
loading ?
|
? arrSkeleton.map((_, index) => <SkeletonTask key={index} />)
|
||||||
arrSkeleton.map((item, index) => {
|
: data.length > 0
|
||||||
return (
|
? data.map((item, index) => (
|
||||||
<SkeletonTask key={index} />
|
<ItemSectionTanggalTugas
|
||||||
)
|
key={index}
|
||||||
})
|
status={item.status}
|
||||||
:
|
title={item.title}
|
||||||
data.length > 0
|
dateStart={item.dateStart}
|
||||||
?
|
dateEnd={item.dateEnd}
|
||||||
data.map((item, index) => {
|
files={item.files ?? []}
|
||||||
return (
|
onPress={() => {
|
||||||
<ItemSectionTanggalTugas
|
setTugas({ id: item.id, status: item.status })
|
||||||
key={index}
|
setModal(true)
|
||||||
done={item.status === 1}
|
}}
|
||||||
title={item.title}
|
/>
|
||||||
dateStart={item.dateStart}
|
))
|
||||||
dateEnd={item.dateEnd}
|
: <Text style={[Styles.textDefault, { textAlign: 'center', color: colors.dimmed }]}>Tidak ada tugas</Text>
|
||||||
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>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<DrawerBottom animation="slide" isVisible={isModal} setVisible={setModal} title="Menu">
|
{/* Drawer menu */}
|
||||||
<View style={Styles.rowItemsCenter}>
|
<DrawerBottom animation="slide" isVisible={isModal} setVisible={setModal} title="Menu" height={40}>
|
||||||
{((entityUser.role != "user" && entityUser.role != "coadmin") || isMemberDivision) && (
|
<View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
|
||||||
<MenuItemRow
|
|
||||||
icon={<MaterialCommunityIcons name="list-status" color={colors.text} size={25} />}
|
{/* Baris 1 — selalu tampil */}
|
||||||
title="Update Status"
|
|
||||||
onPress={() => {
|
|
||||||
setModal(false)
|
|
||||||
setTimeout(() => setSelect(true), 600)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<MenuItemRow
|
<MenuItemRow
|
||||||
icon={<MaterialCommunityIcons name="file-multiple-outline" color={colors.text} size={25} />}
|
icon={<MaterialCommunityIcons name="file-multiple-outline" color={colors.text} size={25} />}
|
||||||
title="File Tugas"
|
title="File Tugas"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setModal(false);
|
setModal(false)
|
||||||
router.push(`/division/${id}/task/${detail}/tugas-file/${tugas.id}?member=${isMemberDivision}`)
|
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} />}
|
icon={<MaterialCommunityIcons name="clock-time-three-outline" color={colors.text} size={25} />}
|
||||||
title="Detail Waktu"
|
title="Detail Waktu"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setModal(false);
|
setModal(false)
|
||||||
setTimeout(() => setModalDetail(true), 600)
|
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>
|
</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>
|
</DrawerBottom>
|
||||||
|
|
||||||
<ModalConfirmation
|
<ModalConfirmation
|
||||||
visible={showDeleteModal}
|
visible={showDeleteModal}
|
||||||
title="Konfirmasi"
|
title="Konfirmasi"
|
||||||
message="Apakah anda yakin ingin menghapus data ini?"
|
message="Apakah anda yakin ingin menghapus data ini?"
|
||||||
onConfirm={() => {
|
onConfirm={() => { setShowDeleteModal(false); handleDelete() }}
|
||||||
setShowDeleteModal(false)
|
|
||||||
handleDelete()
|
|
||||||
}}
|
|
||||||
onCancel={() => setShowDeleteModal(false)}
|
onCancel={() => setShowDeleteModal(false)}
|
||||||
confirmText="Hapus"
|
confirmText="Hapus"
|
||||||
cancelText="Batal"
|
cancelText="Batal"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ModalSelect
|
<ModalConfirmation
|
||||||
category="status-task"
|
visible={modalKonfirmasiSetujui}
|
||||||
close={() => setSelect(false)}
|
title="Konfirmasi"
|
||||||
onSelect={(value) => { handleUpdate(Number(value.val)) }}
|
message="Apakah anda yakin ingin menyetujui tugas ini?"
|
||||||
title="Status"
|
onConfirm={handleSetujui}
|
||||||
open={isSelect}
|
onCancel={() => setModalKonfirmasiSetujui(false)}
|
||||||
valChoose={String(tugas.status)}
|
confirmText="Setujui"
|
||||||
|
cancelText="Batal"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ModalRiwayatApproval
|
||||||
|
isVisible={modalRiwayat}
|
||||||
|
setVisible={setModalRiwayat}
|
||||||
|
data={riwayatData}
|
||||||
|
loading={loadingRiwayat}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ModalTolakApproval
|
||||||
|
isVisible={modalTolak}
|
||||||
|
setVisible={setModalTolak}
|
||||||
|
onTolak={handleTolak}
|
||||||
|
loading={loadingAction}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ModalListDetailTugasTask
|
<ModalListDetailTugasTask
|
||||||
|
|||||||
@@ -1068,6 +1068,51 @@ const Styles = StyleSheet.create({
|
|||||||
color: 'white',
|
color: 'white',
|
||||||
fontWeight: '500',
|
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;
|
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
|
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) => {
|
export const apiEditUser = async (data: FormData, id: string) => {
|
||||||
const response = await api.put(`/mobile/user/${id}`, data, {
|
const response = await api.put(`/mobile/user/${id}`, data, {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -379,6 +384,21 @@ export const apiDeleteProjectTaskFile = async (data: { user: string }, id: strin
|
|||||||
return response.data;
|
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 }) => {
|
export const apiAddMemberProject = async ({ data, id }: { data: { user: string, member: any[] }, id: string }) => {
|
||||||
const response = await api.post(`/mobile/project/${id}/member`, data)
|
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;
|
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 }) => {
|
export const apiGetTugasTaskFile = async ({ user, id }: { user: string, id: string }) => {
|
||||||
const response = await api.get(`/mobile/task/tugas/file/${id}`, { params: { user } })
|
const response = await api.get(`/mobile/task/tugas/file/${id}`, { params: { user } })
|
||||||
return response.data;
|
return response.data;
|
||||||
|
|||||||
Reference in New Issue
Block a user