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:
2026-05-07 16:04:02 +08:00
parent d2e1663f9f
commit e48456ea7f
13 changed files with 811 additions and 289 deletions

View 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>
)
}