amalia/17-apr-26 #37
@@ -3,6 +3,7 @@ import AppHeader from "@/components/AppHeader";
|
|||||||
import BorderBottomItem from "@/components/borderBottomItem";
|
import BorderBottomItem from "@/components/borderBottomItem";
|
||||||
import Skeleton from "@/components/skeleton";
|
import Skeleton from "@/components/skeleton";
|
||||||
import Text from '@/components/Text';
|
import Text from '@/components/Text';
|
||||||
|
import ErrorView from "@/components/ErrorView";
|
||||||
import { ConstEnv } from "@/constants/ConstEnv";
|
import { ConstEnv } from "@/constants/ConstEnv";
|
||||||
import { isImageFile } from "@/constants/FileExtensions";
|
import { isImageFile } from "@/constants/FileExtensions";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
@@ -65,6 +66,7 @@ export default function DetailAnnouncement() {
|
|||||||
const [loadingOpen, setLoadingOpen] = useState(false)
|
const [loadingOpen, setLoadingOpen] = useState(false)
|
||||||
const [preview, setPreview] = useState(false)
|
const [preview, setPreview] = useState(false)
|
||||||
const [chooseFile, setChooseFile] = useState<FileData>()
|
const [chooseFile, setChooseFile] = useState<FileData>()
|
||||||
|
const [isError, setIsError] = useState(false)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens the image preview modal for the selected image file
|
* Opens the image preview modal for the selected image file
|
||||||
@@ -79,6 +81,7 @@ export default function DetailAnnouncement() {
|
|||||||
|
|
||||||
async function handleLoad(loading: boolean) {
|
async function handleLoad(loading: boolean) {
|
||||||
try {
|
try {
|
||||||
|
setIsError(false)
|
||||||
setLoading(loading)
|
setLoading(loading)
|
||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
const response: ApiResponse = await apiGetAnnouncementOne({ id: id, user: hasil })
|
const response: ApiResponse = await apiGetAnnouncementOne({ id: id, user: hasil })
|
||||||
@@ -87,10 +90,12 @@ export default function DetailAnnouncement() {
|
|||||||
setDataMember(response.member)
|
setDataMember(response.member)
|
||||||
setDataFile(response.file)
|
setDataFile(response.file)
|
||||||
} else {
|
} else {
|
||||||
|
setIsError(true)
|
||||||
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);
|
||||||
|
setIsError(true)
|
||||||
const message = error?.response?.data?.message || "Gagal mengambil data"
|
const message = error?.response?.data?.message || "Gagal mengambil data"
|
||||||
|
|
||||||
Toast.show({ type: 'small', text1: message })
|
Toast.show({ type: 'small', text1: message })
|
||||||
@@ -206,104 +211,110 @@ export default function DetailAnnouncement() {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<View style={[Styles.p15, Styles.mb50]}>
|
{isError && !loading ? (
|
||||||
<View style={[Styles.wrapPaper, Styles.borderAll, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]}>
|
<View style={[Styles.mv50]}>
|
||||||
{
|
<ErrorView />
|
||||||
loading ?
|
|
||||||
<View>
|
|
||||||
<View style={[Styles.rowOnly]}>
|
|
||||||
<Skeleton width={30} height={30} borderRadius={10} />
|
|
||||||
<View style={[Styles.flex1, Styles.ph05]}>
|
|
||||||
<Skeleton width={100} widthType="percent" height={30} borderRadius={10} />
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
<Skeleton width={100} widthType="percent" height={10} borderRadius={10} />
|
|
||||||
<Skeleton width={100} widthType="percent" height={10} borderRadius={10} />
|
|
||||||
<Skeleton width={100} widthType="percent" height={10} borderRadius={10} />
|
|
||||||
</View>
|
|
||||||
:
|
|
||||||
<>
|
|
||||||
<View style={[Styles.rowOnly, Styles.alignStart]}>
|
|
||||||
<MaterialIcons name="campaign" size={25} color={colors.text} style={[Styles.mr05]} />
|
|
||||||
<Text style={[Styles.textDefaultSemiBold, Styles.w90, Styles.mt02]}>{data?.title}</Text>
|
|
||||||
</View>
|
|
||||||
<View style={[Styles.mt10]}>
|
|
||||||
{
|
|
||||||
hasHtmlTags(data?.desc) ?
|
|
||||||
<RenderHTML
|
|
||||||
contentWidth={contentWidth}
|
|
||||||
source={{ html: data?.desc }}
|
|
||||||
baseStyle={{ color: colors.text }}
|
|
||||||
/>
|
|
||||||
:
|
|
||||||
<Text>{data?.desc}</Text>
|
|
||||||
}
|
|
||||||
</View>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
{
|
) : (
|
||||||
dataFile.length > 0 && (
|
<View style={[Styles.p15, Styles.mb50]}>
|
||||||
<View style={[Styles.wrapPaper, Styles.borderAll, Styles.mt10, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]}>
|
<View style={[Styles.wrapPaper, Styles.borderAll, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]}>
|
||||||
<View style={[Styles.mb05]}>
|
{
|
||||||
<Text style={[Styles.textDefaultSemiBold]}>File</Text>
|
loading ?
|
||||||
</View>
|
<View>
|
||||||
{dataFile.map((item, index) => (
|
<View style={[Styles.rowOnly]}>
|
||||||
<BorderBottomItem
|
<Skeleton width={30} height={30} borderRadius={10} />
|
||||||
key={`${item.id}-${index}`}
|
<View style={[Styles.flex1, Styles.ph05]}>
|
||||||
borderType={index === dataFile.length - 1 ? 'none' : 'bottom'}
|
<Skeleton width={100} widthType="percent" height={30} borderRadius={10} />
|
||||||
icon={<MaterialCommunityIcons
|
</View>
|
||||||
name={isImageFile(item.extension) ? "file-image-outline" : "file-document-outline"}
|
|
||||||
size={25}
|
|
||||||
color={colors.text}
|
|
||||||
/>}
|
|
||||||
title={item.name + '.' + item.extension}
|
|
||||||
titleWeight="normal"
|
|
||||||
onPress={() => {
|
|
||||||
isImageFile(item.extension) ?
|
|
||||||
handleChooseFile(item)
|
|
||||||
: openFile(item)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
<View style={[Styles.wrapPaper, Styles.borderAll, Styles.mt10, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]}>
|
|
||||||
{
|
|
||||||
loading ?
|
|
||||||
arrSkeleton.map((item, index) => {
|
|
||||||
return (
|
|
||||||
<View key={index}>
|
|
||||||
<Skeleton width={30} widthType="percent" height={10} borderRadius={10} />
|
|
||||||
<Skeleton width={100} widthType="percent" height={10} borderRadius={10} />
|
|
||||||
<Skeleton width={100} widthType="percent" height={10} borderRadius={10} />
|
|
||||||
</View>
|
</View>
|
||||||
)
|
<Skeleton width={100} widthType="percent" height={10} borderRadius={10} />
|
||||||
})
|
<Skeleton width={100} widthType="percent" height={10} borderRadius={10} />
|
||||||
:
|
<Skeleton width={100} widthType="percent" height={10} borderRadius={10} />
|
||||||
Object.keys(dataMember).map((v: any, i: any) => {
|
</View>
|
||||||
return (
|
:
|
||||||
<View key={i} style={[Styles.mb05]}>
|
<>
|
||||||
<Text style={[Styles.textDefaultSemiBold]}>{dataMember[v]?.[0].group}</Text>
|
<View style={[Styles.rowOnly, Styles.alignStart]}>
|
||||||
|
<MaterialIcons name="campaign" size={25} color={colors.text} style={[Styles.mr05]} />
|
||||||
|
<Text style={[Styles.textDefaultSemiBold, Styles.w90, Styles.mt02]}>{data?.title}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[Styles.mt10]}>
|
||||||
{
|
{
|
||||||
dataMember[v].map((item: any, x: any) => {
|
hasHtmlTags(data?.desc) ?
|
||||||
return (
|
<RenderHTML
|
||||||
<View key={x} style={[Styles.rowItemsCenter, Styles.w90]}>
|
contentWidth={contentWidth}
|
||||||
<Entypo name="dot-single" size={24} color={colors.text} />
|
source={{ html: data?.desc }}
|
||||||
<Text style={[Styles.textDefault]} numberOfLines={1} ellipsizeMode='tail'>{item.division}</Text>
|
baseStyle={{ color: colors.text }}
|
||||||
</View>
|
/>
|
||||||
)
|
:
|
||||||
})
|
<Text>{data?.desc}</Text>
|
||||||
}
|
}
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
)
|
</>
|
||||||
})
|
}
|
||||||
|
|
||||||
|
</View>
|
||||||
|
{
|
||||||
|
dataFile.length > 0 && (
|
||||||
|
<View style={[Styles.wrapPaper, Styles.borderAll, Styles.mt10, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]}>
|
||||||
|
<View style={[Styles.mb05]}>
|
||||||
|
<Text style={[Styles.textDefaultSemiBold]}>File</Text>
|
||||||
|
</View>
|
||||||
|
{dataFile.map((item, index) => (
|
||||||
|
<BorderBottomItem
|
||||||
|
key={`${item.id}-${index}`}
|
||||||
|
borderType={index === dataFile.length - 1 ? 'none' : 'bottom'}
|
||||||
|
icon={<MaterialCommunityIcons
|
||||||
|
name={isImageFile(item.extension) ? "file-image-outline" : "file-document-outline"}
|
||||||
|
size={25}
|
||||||
|
color={colors.text}
|
||||||
|
/>}
|
||||||
|
title={item.name + '.' + item.extension}
|
||||||
|
titleWeight="normal"
|
||||||
|
onPress={() => {
|
||||||
|
isImageFile(item.extension) ?
|
||||||
|
handleChooseFile(item)
|
||||||
|
: openFile(item)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
<View style={[Styles.wrapPaper, Styles.borderAll, Styles.mt10, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]}>
|
||||||
|
{
|
||||||
|
loading ?
|
||||||
|
arrSkeleton.map((item, index) => {
|
||||||
|
return (
|
||||||
|
<View key={index}>
|
||||||
|
<Skeleton width={30} widthType="percent" height={10} borderRadius={10} />
|
||||||
|
<Skeleton width={100} widthType="percent" height={10} borderRadius={10} />
|
||||||
|
<Skeleton width={100} widthType="percent" height={10} borderRadius={10} />
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
:
|
||||||
|
Object.keys(dataMember).map((v: any, i: any) => {
|
||||||
|
return (
|
||||||
|
<View key={i} style={[Styles.mb05]}>
|
||||||
|
<Text style={[Styles.textDefaultSemiBold]}>{dataMember[v]?.[0].group}</Text>
|
||||||
|
{
|
||||||
|
dataMember[v].map((item: any, x: any) => {
|
||||||
|
return (
|
||||||
|
<View key={x} style={[Styles.rowItemsCenter, Styles.w90]}>
|
||||||
|
<Entypo name="dot-single" size={24} color={colors.text} />
|
||||||
|
<Text style={[Styles.textDefault]} numberOfLines={1} ellipsizeMode='tail'>{item.division}</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
)}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
<ImageViewing
|
<ImageViewing
|
||||||
|
|||||||
42
components/ErrorView.tsx
Normal file
42
components/ErrorView.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import Text from '@/components/Text';
|
||||||
|
import Styles from '@/constants/Styles';
|
||||||
|
import { useTheme } from '@/providers/ThemeProvider';
|
||||||
|
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||||
|
import React from 'react';
|
||||||
|
import { View } from 'react-native';
|
||||||
|
|
||||||
|
interface ErrorViewProps {
|
||||||
|
title?: string;
|
||||||
|
message?: string;
|
||||||
|
icon?: keyof typeof MaterialCommunityIcons.glyphMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ErrorView component to display error or empty states.
|
||||||
|
* Used when data is not found, deleted, or an error occurs during fetching.
|
||||||
|
*/
|
||||||
|
export default function ErrorView({
|
||||||
|
title = "Terjadi Kesalahan",
|
||||||
|
message = "Data tidak ditemukan atau sudah dihapus.",
|
||||||
|
icon = "alert-circle-outline"
|
||||||
|
}: ErrorViewProps) {
|
||||||
|
const { colors } = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[Styles.flex1, Styles.contentItemCenter, Styles.ph20]}>
|
||||||
|
<View style={[Styles.mb10]}>
|
||||||
|
<MaterialCommunityIcons
|
||||||
|
name={icon}
|
||||||
|
size={40}
|
||||||
|
color={colors.dimmed}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<Text style={[Styles.textDefaultSemiBold, { color: colors.text, textAlign: 'center' }]}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
<Text style={[Styles.textMediumNormal, { color: colors.dimmed, textAlign: 'center', marginTop: 4 }]}>
|
||||||
|
{message}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,10 +7,6 @@ import Text from "../Text";
|
|||||||
export default function ReportChartDocument({ data }: { data: { label: string; value: number; }[] }) {
|
export default function ReportChartDocument({ data }: { data: { label: string; value: number; }[] }) {
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const maxValue = Math.max(...data.map(i => i.value))
|
const maxValue = Math.max(...data.map(i => i.value))
|
||||||
const barData = [
|
|
||||||
{ value: 23, label: 'Gambar', },
|
|
||||||
{ value: 12, label: 'Dokumen' },
|
|
||||||
];
|
|
||||||
const width = Dimensions.get("window").width;
|
const width = Dimensions.get("window").width;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export default function ChartDokumenHome({ refreshing }: { refreshing: boolean }
|
|||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const [data, setData] = useState<Props>([])
|
const [data, setData] = useState<Props>([])
|
||||||
const [maxValue, setMaxValue] = useState(5)
|
const [maxValue, setMaxValue] = useState(5)
|
||||||
|
const [chartKey, setChartKey] = useState(0)
|
||||||
const barData = [
|
const barData = [
|
||||||
{ value: 23, label: 'Gambar', frontColor: '#fac858' },
|
{ value: 23, label: 'Gambar', frontColor: '#fac858' },
|
||||||
{ value: 12, label: 'Dokumen', frontColor: '#92cc76' },
|
{ value: 12, label: 'Dokumen', frontColor: '#92cc76' },
|
||||||
@@ -32,8 +33,8 @@ export default function ChartDokumenHome({ refreshing }: { refreshing: boolean }
|
|||||||
setLoading(loading)
|
setLoading(loading)
|
||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
const response = await apiGetDataHome({ cat: "dokumen", user: hasil })
|
const response = await apiGetDataHome({ cat: "dokumen", user: hasil })
|
||||||
const maxValue = response.data.reduce((max: number, obj: { value: number; }) => Math.max(max, obj.value), -Infinity);
|
const maxVal = response.data.reduce((max: number, obj: { value: number; }) => Math.max(max, Number(obj.value)), 0);
|
||||||
const roundUp = Math.ceil(maxValue / 10) * 10
|
const roundUp = maxVal > 0 ? Math.ceil(maxVal / 10) * 10 : 10;
|
||||||
setMaxValue(roundUp)
|
setMaxValue(roundUp)
|
||||||
const convertedArray = response.data.map((item: { color: any; label: any; value: any; }) => ({
|
const convertedArray = response.data.map((item: { color: any; label: any; value: any; }) => ({
|
||||||
frontColor: item.color,
|
frontColor: item.color,
|
||||||
@@ -41,6 +42,7 @@ export default function ChartDokumenHome({ refreshing }: { refreshing: boolean }
|
|||||||
value: Number(item.value)
|
value: Number(item.value)
|
||||||
}));
|
}));
|
||||||
setData(convertedArray)
|
setData(convertedArray)
|
||||||
|
setChartKey((prev: number) => prev + 1)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -49,8 +51,9 @@ export default function ChartDokumenHome({ refreshing }: { refreshing: boolean }
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (refreshing)
|
if (refreshing) {
|
||||||
handleData(false)
|
handleData(false)
|
||||||
|
}
|
||||||
}, [refreshing]);
|
}, [refreshing]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -64,6 +67,7 @@ export default function ChartDokumenHome({ refreshing }: { refreshing: boolean }
|
|||||||
loading ? <Skeleton width={100} height={200} borderRadius={10} widthType="percent" />
|
loading ? <Skeleton width={100} height={200} borderRadius={10} widthType="percent" />
|
||||||
:
|
:
|
||||||
<BarChart
|
<BarChart
|
||||||
|
key={chartKey}
|
||||||
showFractionalValues={false}
|
showFractionalValues={false}
|
||||||
showYAxisIndices
|
showYAxisIndices
|
||||||
noOfSections={4}
|
noOfSections={4}
|
||||||
|
|||||||
Reference in New Issue
Block a user