feat: tambah fitur approval task pada project dan divisi
- tambah komponen ModalRiwayatApproval dan ModalTolakApproval - update itemSectionTanggalTugas untuk mendukung status menunggu persetujuan - update sectionTanggalTugas (project) dan sectionTanggalTugasTask (divisi) dengan alur approval lengkap - tambah API approval project task dan division task di lib/api.ts - tambah toggle approver di headerMemberDetail dan tampilkan badge approver di detail member - update carouselHome untuk dispatch isApprover ke Redux - update drawerBottom untuk mendukung scroll pada modal - ganti label 'Belum dimulai' menjadi 'Belum ada tugas yang diselesaikan'
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} />
|
||||||
|
|||||||
@@ -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} /> : <></>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -130,11 +131,20 @@ export default function MemberDetail() {
|
|||||||
<View style={[Styles.p15]}>
|
<View style={[Styles.p15]}>
|
||||||
<View style={[Styles.rowSpaceBetween]}>
|
<View style={[Styles.rowSpaceBetween]}>
|
||||||
<Text style={[Styles.textDefaultSemiBold, { color: colors.text }]}>Informasi</Text>
|
<Text style={[Styles.textDefaultSemiBold, { color: colors.text }]}>Informasi</Text>
|
||||||
<LabelStatus
|
<View style={{ flexDirection: 'row', gap: 6 }}>
|
||||||
size="small"
|
{data?.isApprover && (
|
||||||
category={data?.isActive ? 'success' : 'error'}
|
<LabelStatus
|
||||||
text={data?.isActive ? 'AKTIF' : 'TIDAK AKTIF'}
|
size="small"
|
||||||
/>
|
category="primary"
|
||||||
|
text="APPROVER"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<LabelStatus
|
||||||
|
size="small"
|
||||||
|
category={data?.isActive ? 'success' : 'error'}
|
||||||
|
text={data?.isActive ? 'AKTIF' : 'TIDAK AKTIF'}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{
|
{
|
||||||
loading ?
|
loading ?
|
||||||
|
|||||||
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
|
||||||
@@ -243,4 +346,4 @@ export default function SectionTanggalTugasTask({ refreshing, isMemberDivision }
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
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