feat: redesign section progress, report, link, file, dan cancel pada project & division/task

- SectionProgress: progress bar animated, badge persentase, label status, task count
- SectionReport: header ikon, left accent border, TextExpandable dengan label Indonesia
- SectionLink: tap langsung buka URL, ikon per domain, long press untuk hapus
- SectionFile: icon container konsisten 30×30 di semua section
- SectionCancel: card subtle dengan warna error, konsisten dengan visual language baru
- TextExpandable: fix bug show/hide tidak muncul setelah content diupdate
- Tambah 14 style class baru di Styles.ts untuk menggantikan inline style
- Terapkan semua perubahan ke fitur division/task
- Fix menu "Edit Tugas" di sectionTanggalTugasTask yang terpotong karena overflow

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-06 16:22:52 +08:00
parent eccfe29387
commit b61cd51628
16 changed files with 736 additions and 510 deletions

View File

@@ -10,15 +10,13 @@ import { startActivityAsync } from 'expo-intent-launcher';
import { useLocalSearchParams } from "expo-router";
import * as Sharing from 'expo-sharing';
import { useEffect, useState } from "react";
import { Alert, Platform, View } from "react-native";
import { Alert, Platform, Pressable, View } from "react-native";
import * as mime from 'react-native-mime-types';
import Toast from "react-native-toast-message";
import { useDispatch, useSelector } from "react-redux";
import ModalConfirmation from "../ModalConfirmation";
import ButtonMenuHeader from "../buttonMenuHeader";
import BorderBottomItem from "../borderBottomItem";
import DrawerBottom from "../drawerBottom";
import MenuItemRow from "../menuItemRow";
import ModalConfirmation from "../ModalConfirmation";
import ModalLoading from "../modalLoading";
import Skeleton from "../skeleton";
import Text from "../Text";
@@ -30,6 +28,28 @@ type Props = {
idStorage: string
}
function getFileIcon(extension: string): keyof typeof MaterialCommunityIcons.glyphMap {
const ext = extension.toLowerCase()
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'heic', 'heif'].includes(ext)) return 'image-outline'
if (ext === 'pdf') return 'file-pdf-box'
if (['mp4', 'mov', 'avi', 'mkv'].includes(ext)) return 'video-outline'
if (['doc', 'docx'].includes(ext)) return 'file-word-outline'
if (['xls', 'xlsx'].includes(ext)) return 'file-excel-outline'
if (['zip', 'rar', '7z'].includes(ext)) return 'zip-box-outline'
return 'file-outline'
}
function getFileColor(extension: string): string {
const ext = extension.toLowerCase()
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'heic', 'heif'].includes(ext)) return '#339AF0'
if (ext === 'pdf') return '#F03E3E'
if (['mp4', 'mov', 'avi', 'mkv'].includes(ext)) return '#AE3EC9'
if (['doc', 'docx'].includes(ext)) return '#1C7ED6'
if (['xls', 'xlsx'].includes(ext)) return '#2F9E44'
if (['zip', 'rar', '7z'].includes(ext)) return '#E8590C'
return '#868E96'
}
export default function SectionFileTask({ refreshing, isMemberDivision }: { refreshing: boolean, isMemberDivision: boolean }) {
const { colors } = useTheme()
const [isModal, setModal] = useState(false)
@@ -119,7 +139,7 @@ export default function SectionFileTask({ refreshing, isMemberDivision }: { refr
} else {
Toast.show({ type: 'small', text1: response.message, })
}
} catch (error : any ) {
} catch (error: any) {
console.error(error);
const message = error?.response?.data?.message || "Gagal menghapus file"
@@ -134,32 +154,39 @@ export default function SectionFileTask({ refreshing, isMemberDivision }: { refr
<ModalLoading isVisible={loadingOpen} setVisible={setLoadingOpen} />
<View style={[Styles.mb15]}>
<Text style={[Styles.textDefaultSemiBold, Styles.mv05]}>File</Text>
<View style={[Styles.wrapPaper, { backgroundColor: colors.card, borderColor: colors.background }]}>
{
loading ?
arrSkeleton.map((item, index) => {
return (
<Skeleton key={index} width={100} height={40} widthType="percent" borderRadius={10} />
)
})
:
data.length > 0 ?
data.map((item, index) => {
return (
<BorderBottomItem
key={index}
borderType="all"
icon={<MaterialCommunityIcons name="file-outline" size={25} color={colors.text} />}
title={item.name + '.' + item.extension}
titleWeight="normal"
onPress={() => { setSelectFile(item); setModal(true) }}
/>
)
})
:
<Text style={[Styles.textDefault, { textAlign: 'center', color: colors.dimmed }]}>Tidak ada file</Text>
}
</View>
{loading ? (
<View style={Styles.fileGrid}>
{arrSkeleton.map((_, index) => (
<Skeleton key={index} width={48} height={68} widthType="percent" borderRadius={10} />
))}
</View>
) : data.length > 0 ? (
<View style={Styles.fileGrid}>
{data.map((item, index) => {
const iconName = getFileIcon(item.extension)
const iconColor = getFileColor(item.extension)
return (
<Pressable
key={index}
onPress={() => { setSelectFile(item); setModal(true) }}
style={[Styles.fileCard, { backgroundColor: colors.card, borderColor: colors.icon + '18' }]}
>
<View style={[Styles.sectionIconBox, { backgroundColor: iconColor + '20' }]}>
<MaterialCommunityIcons name={iconName} size={18} color={iconColor} />
</View>
<View style={Styles.flex1}>
<Text style={Styles.textDefault} numberOfLines={1}>{item.name}</Text>
<Text style={[Styles.textSmallSemiBold, { color: colors.dimmed }]}>
{item.extension.toUpperCase()}
</Text>
</View>
</Pressable>
)
})}
</View>
) : (
<Text style={[Styles.textDefault, { textAlign: 'center', color: colors.dimmed }]}>Tidak ada file</Text>
)}
</View>
<DrawerBottom animation="slide" isVisible={isModal} setVisible={setModal} title="Menu">

View File

@@ -1,20 +1,19 @@
import Styles from "@/constants/Styles";
import { apiDeleteLinkTask, apiGetTaskOne } from "@/lib/api";
import { urlCompleted } from "@/lib/fun_urlCompleted";
import { setUpdateTask } from "@/lib/taskUpdate";
import { useAuthSession } from "@/providers/AuthProvider";
import { useTheme } from "@/providers/ThemeProvider";
import { Feather, Ionicons } from "@expo/vector-icons";
import { Ionicons } from "@expo/vector-icons";
import { useLocalSearchParams } from "expo-router";
import { useEffect, useState } from "react";
import { Linking, View } from "react-native";
import { View } from "react-native";
import Toast from "react-native-toast-message";
import { useDispatch, useSelector } from "react-redux";
import ModalConfirmation from "../ModalConfirmation";
import BorderBottomItem from "../borderBottomItem";
import DrawerBottom from "../drawerBottom";
import MenuItemRow from "../menuItemRow";
import Text from "../Text";
import ItemSectionLink from "../project/itemSectionLink";
type Props = {
id: string
@@ -62,84 +61,60 @@ export default function SectionLinkTask({ refreshing, isMemberDivision }: { refr
} else {
Toast.show({ type: 'small', text1: response.message, })
}
} catch (error : any ) {
} catch (error: any) {
console.error(error);
const message = error?.response?.data?.message || "Gagal menghapus link"
Toast.show({ type: 'small', text1: message })
} finally {
setModal(false)
}
}
if (data.length === 0) return null;
const canDelete = (entityUser.role !== "user" && entityUser.role !== "coadmin") || isMemberDivision;
return (
<>
{
data.length > 0 &&
<>
<View style={[Styles.mb15]}>
<Text style={[Styles.textDefaultSemiBold, Styles.mv05]}>Link</Text>
<View style={[Styles.wrapPaper, { backgroundColor: colors.card, borderColor: colors.background }]}>
{
data.map((item, index) => {
return (
<BorderBottomItem
key={index}
borderType="all"
icon={<Feather name="link" size={25} color={colors.text} />}
title={item.link}
titleWeight="normal"
onPress={() => { setSelectLink(item); setModal(true) }}
/>
)
})
}
</View>
</View>
<View style={[Styles.mb15]}>
<Text style={[Styles.textDefaultSemiBold, Styles.mv05]}>Link</Text>
<View>
{data.map((item, index) => (
<ItemSectionLink
key={index}
link={item.link}
canDelete={canDelete}
onLongPress={() => { setSelectLink(item); setModal(true) }}
/>
))}
</View>
</View>
<DrawerBottom animation="slide" isVisible={isModal} setVisible={setModal} title="Menu">
<View style={Styles.rowItemsCenter}>
<MenuItemRow
icon={<Feather name="external-link" color={colors.text} size={25} />}
title="Buka Link"
onPress={() => {
Linking.openURL(urlCompleted(String(selectLink?.link)))
}}
/>
{
(entityUser.role != "user" && entityUser.role != "coadmin") || isMemberDivision
?
<MenuItemRow
icon={<Ionicons name="trash-outline" color={colors.text} size={25} />}
title="Hapus"
onPress={() => {
setModal(false)
setTimeout(() => {
setShowDeleteModal(true)
}, 600)
}}
/>
:
<></>
}
</View>
</DrawerBottom>
<ModalConfirmation
visible={showDeleteModal}
title="Konfirmasi"
message="Apakah Anda yakin ingin menghapus link ini? Link yang dihapus tidak dapat dikembalikan"
onConfirm={() => {
setShowDeleteModal(false)
handleDelete()
<DrawerBottom animation="slide" isVisible={isModal} setVisible={setModal} title="Menu">
<View style={Styles.rowItemsCenter}>
<MenuItemRow
icon={<Ionicons name="trash-outline" color={colors.text} size={25} />}
title="Hapus Link"
onPress={() => {
setModal(false)
setTimeout(() => setShowDeleteModal(true), 600)
}}
onCancel={() => setShowDeleteModal(false)}
confirmText="Hapus"
cancelText="Batal"
/>
</>
}
</View>
</DrawerBottom>
<ModalConfirmation
visible={showDeleteModal}
title="Konfirmasi"
message="Apakah Anda yakin ingin menghapus link ini? Link yang dihapus tidak dapat dikembalikan"
onConfirm={() => {
setShowDeleteModal(false)
handleDelete()
}}
onCancel={() => setShowDeleteModal(false)}
confirmText="Hapus"
cancelText="Batal"
/>
</>
)
}
}

View File

@@ -2,6 +2,7 @@ import Styles from "@/constants/Styles";
import { apiGetTaskOne } from "@/lib/api";
import { useAuthSession } from "@/providers/AuthProvider";
import { useTheme } from "@/providers/ThemeProvider";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import { useLocalSearchParams } from "expo-router";
import { useEffect, useState } from "react";
import { View } from "react-native";
@@ -9,7 +10,6 @@ import { useSelector } from "react-redux";
import Text from "../Text";
import TextExpandable from "../textExpandable";
export default function SectionReportTask({ refreshing }: { refreshing: boolean }) {
const { colors } = useTheme()
const update = useSelector((state: any) => state.taskUpdate)
@@ -27,29 +27,30 @@ export default function SectionReportTask({ refreshing }: { refreshing: boolean
}
}
useEffect(() => {
handleLoad()
}, [update.report])
useEffect(() => {
if (refreshing)
handleLoad();
}, [refreshing]);
useEffect(() => { handleLoad() }, [update.report])
useEffect(() => { if (refreshing) handleLoad() }, [refreshing])
if (!data || data === "") return null;
return (
<>
{
data != "" && data != null &&
<View style={[Styles.mb15, Styles.mt10]}>
<Text style={[Styles.textDefaultSemiBold, Styles.mv05]}>
Laporan Kegiatan
</Text>
<View style={[Styles.wrapPaper, { backgroundColor: colors.card, borderColor: colors.background }]}>
<TextExpandable content={data} maxLines={2} />
</View>
<View style={[
Styles.wrapPaper,
Styles.mb15,
Styles.sectionCard,
{ backgroundColor: colors.card, borderColor: colors.icon + '18' },
]}>
<View style={Styles.sectionHeader}>
<View style={[Styles.sectionIconBox, Styles.mr10, { backgroundColor: colors.tabActive + '18' }]}>
<MaterialCommunityIcons name="text-box-outline" size={18} color={colors.tabActive} />
</View>
}
</>
<Text style={[Styles.textDefaultSemiBold, { color: colors.text }]}>
Laporan Kegiatan
</Text>
</View>
<View style={[Styles.reportContent, { borderLeftColor: colors.tabActive + '50' }]}>
<TextExpandable content={data} maxLines={2} />
</View>
</View>
)
}
}

View File

@@ -9,10 +9,10 @@ import { useEffect, useState } from "react";
import { View } from "react-native";
import Toast from "react-native-toast-message";
import { useDispatch, useSelector } from "react-redux";
import ModalConfirmation from "../ModalConfirmation";
import DrawerBottom from "../drawerBottom";
import ItemSectionTanggalTugas from "../itemSectionTanggalTugas";
import MenuItemRow from "../menuItemRow";
import ModalConfirmation from "../ModalConfirmation";
import ModalSelect from "../modalSelect";
import SkeletonTask from "../skeletonTask";
import Text from "../Text";
@@ -74,7 +74,7 @@ export default function SectionTanggalTugasTask({ refreshing, isMemberDivision }
} else {
Toast.show({ type: 'small', text1: response.message, })
}
} catch (error : any ) {
} catch (error: any) {
console.error(error);
const message = error?.response?.data?.message || "Gagal mengubah data"
@@ -112,7 +112,7 @@ export default function SectionTanggalTugasTask({ refreshing, isMemberDivision }
} else {
Toast.show({ type: 'small', text1: response.message, })
}
} catch (error : any ) {
} catch (error: any) {
console.error(error);
const message = error?.response?.data?.message || "Gagal menghapus data"
@@ -127,7 +127,7 @@ export default function SectionTanggalTugasTask({ refreshing, isMemberDivision }
<>
<View style={[Styles.mb15, Styles.mt10]}>
<Text style={[Styles.textDefaultSemiBold, Styles.mv05]}>Tanggal & Tugas</Text>
<View style={[Styles.wrapPaper, { backgroundColor: colors.card, borderColor: colors.background }]}>
<View>
{
loading ?
arrSkeleton.map((item, index) => {
@@ -165,22 +165,16 @@ export default function SectionTanggalTugasTask({ refreshing, isMemberDivision }
<DrawerBottom animation="slide" isVisible={isModal} setVisible={setModal} title="Menu">
<View style={Styles.rowItemsCenter}>
<MenuItemRow
icon={
<MaterialCommunityIcons
name="clock-time-three-outline"
color={colors.text}
size={25}
/>
}
title="Detail Waktu"
onPress={() => {
setModal(false);
setTimeout(() => {
setModalDetail(true)
}, 600)
}}
/>
{((entityUser.role != "user" && entityUser.role != "coadmin") || isMemberDivision) && (
<MenuItemRow
icon={<MaterialCommunityIcons name="list-status" color={colors.text} size={25} />}
title="Update Status"
onPress={() => {
setModal(false)
setTimeout(() => setSelect(true), 600)
}}
/>
)}
<MenuItemRow
icon={<MaterialCommunityIcons name="file-multiple-outline" color={colors.text} size={25} />}
title="File Tugas"
@@ -189,52 +183,35 @@ export default function SectionTanggalTugasTask({ refreshing, isMemberDivision }
router.push(`/division/${id}/task/${detail}/tugas-file/${tugas.id}?member=${isMemberDivision}`)
}}
/>
{
(entityUser.role != "user" && entityUser.role != "coadmin") || isMemberDivision
?
<>
<MenuItemRow
icon={<MaterialCommunityIcons name="list-status" color={colors.text} size={25} />}
title="Update Status"
onPress={() => {
setModal(false)
setTimeout(() => {
setSelect(true)
}, 600);
}}
/>
<MenuItemRow
icon={<MaterialCommunityIcons name="pencil-outline" color={colors.text} size={25} />}
title="Edit Tugas"
onPress={() => {
setModal(false)
router.push(`./update/${tugas.id}`)
}}
/>
</>
:
<></>
}
<MenuItemRow
icon={<MaterialCommunityIcons name="clock-time-three-outline" color={colors.text} size={25} />}
title="Detail Waktu"
onPress={() => {
setModal(false);
setTimeout(() => setModalDetail(true), 600)
}}
/>
</View>
{
(entityUser.role != "user" && entityUser.role != "coadmin") || isMemberDivision
?
<View style={[Styles.rowItemsCenter, Styles.mt15]}>
<MenuItemRow
icon={<Ionicons name="trash-outline" color={colors.text} size={25} />}
title="Hapus Tugas"
onPress={() => {
setModal(false)
setTimeout(() => {
setShowDeleteModal(true)
}, 600)
}}
/>
</View>
:
<></>
}
{((entityUser.role != "user" && entityUser.role != "coadmin") || isMemberDivision) && (
<View style={[Styles.rowItemsCenter, Styles.mt15]}>
<MenuItemRow
icon={<MaterialCommunityIcons name="pencil-outline" color={colors.text} size={25} />}
title="Edit Tugas"
onPress={() => {
setModal(false)
router.push(`./update/${tugas.id}`)
}}
/>
<MenuItemRow
icon={<Ionicons name="trash-outline" color={colors.text} size={25} />}
title="Hapus Tugas"
onPress={() => {
setModal(false)
setTimeout(() => setShowDeleteModal(true), 600)
}}
/>
</View>
)}
</DrawerBottom>
<ModalConfirmation