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.
299 lines
14 KiB
TypeScript
299 lines
14 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 { apiCreateAnnouncement } 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 } from "expo-router";
|
|
import React, { 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'
|
|
}
|
|
|
|
export default function CreateAnnouncement() {
|
|
const dispatch = useDispatch()
|
|
const update = useSelector((state: any) => state.announcementUpdate)
|
|
const { token, decryptToken } = useAuthSession()
|
|
const { colors } = useTheme();
|
|
const [disableBtn, setDisableBtn] = useState(true);
|
|
const [modalDivisi, setModalDivisi] = useState(false);
|
|
const [divisionMember, setDivisionMember] = useState<any[]>([])
|
|
const [loading, setLoading] = useState(false)
|
|
const [fileForm, setFileForm] = useState<any[]>([])
|
|
const [isModalFile, setModalFile] = useState(false)
|
|
const [indexDelFile, setIndexDelFile] = useState<number>(0)
|
|
const [dataForm, setDataForm] = useState({ title: "", desc: "" });
|
|
const [error, setError] = useState({ title: false, desc: false });
|
|
|
|
const totalDivisi = divisionMember.reduce((acc: number, g: any) => acc + g.Division.length, 0)
|
|
|
|
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 handleCreate() {
|
|
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({ user: hasil, groups: divisionMember, ...dataForm }))
|
|
const response = await apiCreateAnnouncement(fd)
|
|
if (response.success) {
|
|
dispatch(setUpdateAnnouncement(!update))
|
|
Toast.show({ type: 'small', text1: 'Berhasil menambahkan data' })
|
|
router.back();
|
|
} else {
|
|
Toast.show({ type: 'small', text1: response.message })
|
|
}
|
|
} catch (error: any) {
|
|
console.error(error);
|
|
Toast.show({ type: 'small', text1: error?.response?.data?.message || "Tidak dapat terhubung ke server" })
|
|
} 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
|
|
if (fileForm.some(f => f.name === asset.name)) {
|
|
skipped++
|
|
} else {
|
|
setFileForm(prev => [...prev, asset])
|
|
}
|
|
}
|
|
if (skipped > 0) Toast.show({ type: 'small', text1: 'Beberapa file sudah ditambahkan' })
|
|
}
|
|
};
|
|
|
|
function deleteFile(index: number) {
|
|
setFileForm(fileForm.filter((_, i) => i !== index))
|
|
setModalFile(false)
|
|
}
|
|
|
|
return (
|
|
<SafeAreaView style={[Styles.flex1, { backgroundColor: colors.background }]}>
|
|
<Stack.Screen
|
|
options={{
|
|
header: () => (
|
|
<AppHeader
|
|
title="Tambah Pengumuman"
|
|
showBack={true}
|
|
onPressLeft={() => router.back()}
|
|
right={
|
|
<ButtonSaveHeader
|
|
disable={disableBtn || divisionMember.length === 0 || loading}
|
|
category="create"
|
|
onPress={() => {
|
|
divisionMember.length === 0
|
|
? Toast.show({ type: 'small', text1: "Anda belum memilih divisi" })
|
|
: handleCreate()
|
|
}}
|
|
/>
|
|
}
|
|
/>
|
|
)
|
|
}}
|
|
/>
|
|
{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)}
|
|
/>
|
|
|
|
<InputForm
|
|
label="Pengumuman"
|
|
type="default"
|
|
placeholder="Deskripsi Pengumuman"
|
|
required
|
|
error={error.desc}
|
|
bg={colors.card}
|
|
errorText="Pengumuman harus diisi"
|
|
onChange={(val) => validationForm("desc", val)}
|
|
multiline
|
|
/>
|
|
|
|
{/* File */}
|
|
<View style={[Styles.wrapPaper, Styles.mb15, Styles.sectionCard,
|
|
{ backgroundColor: colors.card, borderColor: colors.icon + '18' }]}>
|
|
<Pressable
|
|
onPress={pickDocumentAsync}
|
|
style={[Styles.sectionActionRow, { marginBottom: fileForm.length > 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>
|
|
{fileForm.length === 0 && (
|
|
<Text style={[Styles.textMediumNormal, { color: colors.dimmed }]}>Opsional — ketuk untuk upload</Text>
|
|
)}
|
|
</View>
|
|
{fileForm.length > 0 && (
|
|
<View style={[Styles.sectionBadge, { backgroundColor: colors.dimmed + '18' }]}>
|
|
<Text style={[Styles.textSmallSemiBold, { color: colors.dimmed }]}>{fileForm.length} file</Text>
|
|
</View>
|
|
)}
|
|
<MaterialCommunityIcons name="chevron-right" size={18} color={colors.dimmed} />
|
|
</Pressable>
|
|
{fileForm.length > 0 && (
|
|
<View style={Styles.fileGrid}>
|
|
{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={index}
|
|
onPress={() => { setIndexDelFile(index); 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: divisionMember.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>
|
|
{divisionMember.length === 0 && (
|
|
<Text style={[Styles.textMediumNormal, { color: colors.dimmed }]}>Belum ada divisi dipilih</Text>
|
|
)}
|
|
</View>
|
|
{divisionMember.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>
|
|
{divisionMember.length > 0 && (
|
|
<View style={{ gap: 10 }}>
|
|
{divisionMember.map((item: any, index: number) => (
|
|
<View key={index}>
|
|
<Text style={[Styles.textMediumNormal, { color: colors.dimmed, marginBottom: 4 }]}>
|
|
{item.name}
|
|
</Text>
|
|
<View style={{ gap: 6 }}>
|
|
{item.Division.map((division: any, i: number) => (
|
|
<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) => {
|
|
setDivisionMember(val)
|
|
setModalDivisi(false)
|
|
}}
|
|
/>
|
|
|
|
<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)}
|
|
/>
|
|
</View>
|
|
</DrawerBottom>
|
|
</SafeAreaView>
|
|
);
|
|
}
|