Cek nama file sebelum menambahkan ke list, skip jika sudah ada. Gunakan nama file (bukan URI) karena Android dapat menghasilkan URI berbeda untuk file yang sama di setiap sesi picker.
357 lines
17 KiB
TypeScript
357 lines
17 KiB
TypeScript
import AppHeader from "@/components/AppHeader";
|
|
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
|
import DrawerBottom from "@/components/drawerBottom";
|
|
import { InputForm } from "@/components/inputForm";
|
|
import LoadingCenter from "@/components/loadingCenter";
|
|
import MenuItemRow from "@/components/menuItemRow";
|
|
import ModalSelectMultiple from "@/components/modalSelectMultiple";
|
|
import Text from '@/components/Text';
|
|
import Styles from "@/constants/Styles";
|
|
import { setUpdateAnnouncement } from "@/lib/announcementUpdate";
|
|
import { apiEditAnnouncement, apiGetAnnouncementOne } from "@/lib/api";
|
|
import { useAuthSession } from "@/providers/AuthProvider";
|
|
import { useTheme } from "@/providers/ThemeProvider";
|
|
import { Ionicons, MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons";
|
|
import * as DocumentPicker from "expo-document-picker";
|
|
import { router, Stack, useLocalSearchParams } from "expo-router";
|
|
import { useEffect, useState } from "react";
|
|
import { Pressable, SafeAreaView, ScrollView, View } from "react-native";
|
|
import Toast from "react-native-toast-message";
|
|
import { useDispatch, useSelector } from "react-redux";
|
|
|
|
function getFileIcon(ext: string): keyof typeof MaterialCommunityIcons.glyphMap {
|
|
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(ext: string): string {
|
|
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'
|
|
}
|
|
|
|
type GroupDivision = {
|
|
id: string;
|
|
name: string;
|
|
Division: { id: string; name: string }[];
|
|
}
|
|
|
|
export default function EditAnnouncement() {
|
|
const { id } = useLocalSearchParams<{ id: string }>();
|
|
const dispatch = useDispatch()
|
|
const update = useSelector((state: any) => state.announcementUpdate)
|
|
const { token, decryptToken } = useAuthSession();
|
|
const { colors } = useTheme();
|
|
const [modalDivisi, setModalDivisi] = useState(false);
|
|
const [disableBtn, setDisableBtn] = useState(true);
|
|
const [dataMember, setDataMember] = useState<GroupDivision[]>([]);
|
|
const [fileForm, setFileForm] = useState<any[]>([])
|
|
const [dataFile, setDataFile] = useState<{ id: string; idStorage: string; name: string; extension: string; delete?: boolean }[]>([])
|
|
const [indexDelFile, setIndexDelFile] = useState<{ id: string | number; cat: "newFile" | "oldFile" }>({ id: "", cat: "newFile" })
|
|
const [isModalFile, setModalFile] = useState(false)
|
|
const [loading, setLoading] = useState(false)
|
|
const [dataForm, setDataForm] = useState({ title: "", desc: "" });
|
|
const [error, setError] = useState({ title: false, desc: false });
|
|
|
|
const visibleOldFiles = dataFile.filter(v => !v.delete)
|
|
const totalFiles = fileForm.length + visibleOldFiles.length
|
|
const totalDivisi = dataMember.reduce((acc: number, g: any) => acc + g.Division.length, 0)
|
|
|
|
async function handleLoad() {
|
|
try {
|
|
const hasil = await decryptToken(String(token?.current));
|
|
const response = await apiGetAnnouncementOne({ id: id, user: hasil });
|
|
setDataForm(response.data);
|
|
const arrNew: GroupDivision[] = Object.keys(response.member).map((v) => ({
|
|
id: response.member[v][0].idGroup,
|
|
name: v,
|
|
Division: response.member[v].map((m: any) => ({ id: m.idDivision, name: m.division }))
|
|
}))
|
|
setDataMember(arrNew);
|
|
setDataFile(response.file);
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
}
|
|
|
|
useEffect(() => { handleLoad() }, []);
|
|
|
|
function validationForm(cat: string, val: any) {
|
|
if (cat === "title") {
|
|
setDataForm({ ...dataForm, title: val });
|
|
setError({ ...error, title: val === "" || val === "null" });
|
|
} else if (cat === "desc") {
|
|
setDataForm({ ...dataForm, desc: val });
|
|
setError({ ...error, desc: val === "" || val === "null" });
|
|
}
|
|
}
|
|
|
|
function checkForm() {
|
|
const hasError = Object.values(error).some(v => v)
|
|
const hasEmpty = Object.values(dataForm).some(v => v === "")
|
|
setDisableBtn(hasError || hasEmpty);
|
|
}
|
|
|
|
useEffect(() => { checkForm() }, [error, dataForm]);
|
|
|
|
async function handleEdit() {
|
|
try {
|
|
setLoading(true)
|
|
const hasil = await decryptToken(String(token?.current))
|
|
const fd = new FormData()
|
|
for (let i = 0; i < fileForm.length; i++) {
|
|
fd.append(`file${i}`, { uri: fileForm[i].uri, type: 'application/octet-stream', name: fileForm[i].name } as any);
|
|
}
|
|
fd.append("data", JSON.stringify({ ...dataForm, user: hasil, groups: dataMember, oldFile: dataFile }))
|
|
const response = await apiEditAnnouncement(fd, id);
|
|
if (response.success) {
|
|
dispatch(setUpdateAnnouncement(!update))
|
|
Toast.show({ type: 'small', text1: 'Berhasil mengubah data' })
|
|
router.back();
|
|
} else {
|
|
Toast.show({ type: 'small', text1: 'Gagal mengubah data' })
|
|
}
|
|
} catch (error: any) {
|
|
console.error(error);
|
|
Toast.show({ type: 'small', text1: error?.response?.data?.message || "Gagal mengubah data" })
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const pickDocumentAsync = async () => {
|
|
const result = await DocumentPicker.getDocumentAsync({ type: ["*/*"], multiple: true });
|
|
if (!result.canceled) {
|
|
let skipped = 0
|
|
for (const asset of result.assets) {
|
|
if (!asset.uri) continue
|
|
const isDup = fileForm.some(f => f.name === asset.name) ||
|
|
visibleOldFiles.some(f => `${f.name}.${f.extension}` === asset.name)
|
|
if (isDup) {
|
|
skipped++
|
|
} else {
|
|
setFileForm(prev => [...prev, asset])
|
|
}
|
|
}
|
|
if (skipped > 0) Toast.show({ type: 'small', text1: 'Beberapa file sudah ditambahkan' })
|
|
}
|
|
};
|
|
|
|
function deleteFile(index: number | string, cat: "newFile" | "oldFile" | null) {
|
|
if (cat === "newFile") {
|
|
setFileForm(fileForm.filter((_, i) => i !== index))
|
|
} else {
|
|
setDataFile(prev => prev.map(item => item.id === index ? { ...item, delete: true } : item))
|
|
}
|
|
setModalFile(false)
|
|
}
|
|
|
|
return (
|
|
<SafeAreaView style={[Styles.flex1, { backgroundColor: colors.background }]}>
|
|
<Stack.Screen
|
|
options={{
|
|
header: () => (
|
|
<AppHeader
|
|
title="Edit Pengumuman"
|
|
showBack={true}
|
|
onPressLeft={() => router.back()}
|
|
right={
|
|
<ButtonSaveHeader
|
|
disable={disableBtn || dataMember.length === 0 || loading}
|
|
category="update"
|
|
onPress={() => {
|
|
dataMember.length === 0
|
|
? Toast.show({ type: 'small', text1: "Anda belum memilih divisi" })
|
|
: handleEdit()
|
|
}}
|
|
/>
|
|
}
|
|
/>
|
|
)
|
|
}}
|
|
/>
|
|
{loading && <LoadingCenter />}
|
|
<ScrollView showsVerticalScrollIndicator={false} style={[Styles.h100, { backgroundColor: colors.background }]}>
|
|
<View style={Styles.p15}>
|
|
|
|
<InputForm
|
|
label="Judul"
|
|
type="default"
|
|
placeholder="Judul Pengumuman"
|
|
required
|
|
error={error.title}
|
|
bg={colors.card}
|
|
errorText="Judul harus diisi"
|
|
onChange={(val) => validationForm("title", val)}
|
|
value={dataForm.title}
|
|
/>
|
|
|
|
<InputForm
|
|
label="Pengumuman"
|
|
type="default"
|
|
placeholder="Deskripsi Pengumuman"
|
|
required
|
|
error={error.desc}
|
|
bg={colors.card}
|
|
errorText="Pengumuman harus diisi"
|
|
onChange={(val) => validationForm("desc", val)}
|
|
value={dataForm.desc}
|
|
multiline
|
|
/>
|
|
|
|
{/* File */}
|
|
<View style={[Styles.wrapPaper, Styles.mb15, Styles.sectionCard,
|
|
{ backgroundColor: colors.card, borderColor: colors.icon + '18' }]}>
|
|
<Pressable
|
|
onPress={pickDocumentAsync}
|
|
style={[Styles.sectionActionRow, { marginBottom: totalFiles > 0 ? 12 : 0 }]}
|
|
>
|
|
<View style={[Styles.sectionIconBox, { backgroundColor: colors.icon + '15' }]}>
|
|
<MaterialCommunityIcons name="paperclip" size={18} color={colors.dimmed} />
|
|
</View>
|
|
<View style={Styles.flex1}>
|
|
<Text style={[Styles.textDefaultSemiBold, { color: colors.text }]}>File</Text>
|
|
{totalFiles === 0 && (
|
|
<Text style={[Styles.textMediumNormal, { color: colors.dimmed }]}>Opsional — ketuk untuk upload</Text>
|
|
)}
|
|
</View>
|
|
{totalFiles > 0 && (
|
|
<View style={[Styles.sectionBadge, { backgroundColor: colors.dimmed + '18' }]}>
|
|
<Text style={[Styles.textSmallSemiBold, { color: colors.dimmed }]}>{totalFiles} file</Text>
|
|
</View>
|
|
)}
|
|
<MaterialCommunityIcons name="chevron-right" size={18} color={colors.dimmed} />
|
|
</Pressable>
|
|
{totalFiles > 0 && (
|
|
<View style={Styles.fileGrid}>
|
|
{visibleOldFiles.map((item, index) => {
|
|
const ext = item.extension.toLowerCase()
|
|
const iconName = getFileIcon(ext)
|
|
const iconColor = getFileColor(ext)
|
|
return (
|
|
<Pressable
|
|
key={`old-${index}`}
|
|
onPress={() => { setIndexDelFile({ id: item.id, cat: "oldFile" }); setModalFile(true) }}
|
|
style={[Styles.fileCard, { backgroundColor: 'transparent', 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 }]}>{ext.toUpperCase()}</Text>
|
|
</View>
|
|
</Pressable>
|
|
)
|
|
})}
|
|
{fileForm.map((item, index) => {
|
|
const ext = item.name.split('.').pop()?.toLowerCase() ?? ''
|
|
const baseName = item.name.includes('.') ? item.name.split('.').slice(0, -1).join('.') : item.name
|
|
const iconName = getFileIcon(ext)
|
|
const iconColor = getFileColor(ext)
|
|
return (
|
|
<Pressable
|
|
key={`new-${index}`}
|
|
onPress={() => { setIndexDelFile({ id: index, cat: "newFile" }); setModalFile(true) }}
|
|
style={[Styles.fileCard, { backgroundColor: 'transparent', 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}>{baseName}</Text>
|
|
<Text style={[Styles.textSmallSemiBold, { color: colors.dimmed }]}>{ext.toUpperCase()}</Text>
|
|
</View>
|
|
</Pressable>
|
|
)
|
|
})}
|
|
</View>
|
|
)}
|
|
</View>
|
|
|
|
{/* Divisi Penerima */}
|
|
<View style={[Styles.wrapPaper, Styles.mb15, Styles.sectionCard,
|
|
{ backgroundColor: colors.card, borderColor: colors.icon + '18' }]}>
|
|
<Pressable
|
|
onPress={() => setModalDivisi(true)}
|
|
style={[Styles.sectionActionRow, { marginBottom: dataMember.length > 0 ? 12 : 0 }]}
|
|
>
|
|
<View style={[Styles.sectionIconBox, { backgroundColor: colors.tabActive + '18' }]}>
|
|
<MaterialIcons name="groups" size={18} color={colors.tabActive} />
|
|
</View>
|
|
<View style={Styles.flex1}>
|
|
<Text style={[Styles.textDefaultSemiBold, { color: colors.text }]}>Divisi Penerima</Text>
|
|
{dataMember.length === 0 && (
|
|
<Text style={[Styles.textMediumNormal, { color: colors.dimmed }]}>Belum ada divisi dipilih</Text>
|
|
)}
|
|
</View>
|
|
{dataMember.length > 0 && (
|
|
<View style={[Styles.sectionBadge, { backgroundColor: colors.tabActive + '18' }]}>
|
|
<Text style={[Styles.textSmallSemiBold, { color: colors.tabActive }]}>{totalDivisi} divisi</Text>
|
|
</View>
|
|
)}
|
|
<MaterialCommunityIcons name="chevron-right" size={18} color={colors.dimmed} />
|
|
</Pressable>
|
|
{dataMember.length > 0 && (
|
|
<View style={{ gap: 10 }}>
|
|
{dataMember.map((item, index) => (
|
|
<View key={index}>
|
|
<Text style={[Styles.textMediumNormal, { color: colors.dimmed, marginBottom: 4 }]}>
|
|
{item.name}
|
|
</Text>
|
|
<View style={{ gap: 6 }}>
|
|
{item.Division.map((division, i) => (
|
|
<View key={i} style={[Styles.listItemCard, { borderColor: colors.icon + '18' }]}>
|
|
<View style={[Styles.sectionIconBox, { backgroundColor: colors.tabActive + '18', width: 28, height: 28, borderRadius: 8 }]}>
|
|
<MaterialIcons name="group" size={14} color={colors.tabActive} />
|
|
</View>
|
|
<Text style={[Styles.textDefault, Styles.flex1, { color: colors.text }]} numberOfLines={1}>
|
|
{division.name}
|
|
</Text>
|
|
</View>
|
|
))}
|
|
</View>
|
|
</View>
|
|
))}
|
|
</View>
|
|
)}
|
|
</View>
|
|
|
|
</View>
|
|
</ScrollView>
|
|
|
|
<ModalSelectMultiple
|
|
choose="dinas"
|
|
title="Pilih Divisi"
|
|
category="choose-division"
|
|
open={modalDivisi}
|
|
close={setModalDivisi}
|
|
onSelect={(val) => {
|
|
setDataMember(val)
|
|
setModalDivisi(false)
|
|
}}
|
|
value={dataMember}
|
|
/>
|
|
|
|
<DrawerBottom animation="slide" isVisible={isModalFile} setVisible={setModalFile} title="Menu">
|
|
<View style={Styles.rowItemsCenter}>
|
|
<MenuItemRow
|
|
icon={<Ionicons name="trash-outline" color={colors.text} size={25} />}
|
|
title="Hapus"
|
|
onPress={() => deleteFile(indexDelFile.id, indexDelFile.cat)}
|
|
/>
|
|
</View>
|
|
</DrawerBottom>
|
|
</SafeAreaView>
|
|
);
|
|
}
|