Compare commits

...

6 Commits

Author SHA1 Message Date
eee3691aca upd: diskusi umum
Deskripsi:
- detail open file pada halaman detaul diskusi umum
- upload dan hapus file pada halaman edit diskusi umum

- refresh halaman detail diskusi ummum

No Issues
2026-01-17 11:19:24 +08:00
d9508ba978 Merge pull request 'upd: diskusi umum' (#7) from amalia/15-jan-26 into join
Reviewed-on: http://wibugit.wibudev.com/wibu/mobile-darmasaba/pulls/7
2026-01-15 17:36:18 +08:00
6f3514c80c upd: diskusi umum
deskripsi :
- integrasi api tambah data diskusi umum
- integrasi api detail data diskusi umum file

No Issues
2026-01-15 17:33:05 +08:00
bd31a2f993 Merge pull request 'req: pengumuman' (#6) from amalia/14-jan-26 into join
Reviewed-on: http://wibugit.wibudev.com/wibu/mobile-darmasaba/pulls/6
2026-01-14 17:45:05 +08:00
3fbb230302 req: pengumuman
Deskripsi:
- pengaplikasian api tambah, detail dan edit pengumuman

No Issues
2026-01-14 15:01:32 +08:00
57a24b699a Merge pull request 'tambahan:' (#5) from amalia/13-jan-26 into join
Reviewed-on: http://wibugit.wibudev.com/wibu/mobile-darmasaba/pulls/5
2026-01-13 17:14:05 +08:00
8 changed files with 284 additions and 57 deletions

View File

@@ -30,7 +30,7 @@ export default function DetailAnnouncement() {
const { token, decryptToken } = useAuthSession()
const [data, setData] = useState<Props>({ id: '', title: '', desc: '' })
const [dataMember, setDataMember] = useState<any>({})
const [dataFile, setDataFile] = useState<{ idStorage: string; name: string; extension: string }[]>([])
const [dataFile, setDataFile] = useState<{ id:string; idStorage: string; name: string; extension: string }[]>([])
const update = useSelector((state: any) => state.announcementUpdate)
const entityUser = useSelector((state: any) => state.user)
const contentWidth = Dimensions.get('window').width
@@ -48,6 +48,7 @@ export default function DetailAnnouncement() {
if (response.success) {
setData(response.data)
setDataMember(response.member)
setDataFile(response.file)
} else {
Toast.show({ type: 'small', text1: response.message })
}

View File

@@ -76,9 +76,26 @@ export default function CreateAnnouncement() {
try {
setLoading(true)
const hasil = await decryptToken(String(token?.current))
const response = await apiCreateAnnouncement({
data: { ...dataForm, user: hasil, groups: divisionMember },
});
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)
// const response = await apiCreateAnnouncement({
// data: { ...dataForm, user: hasil, groups: divisionMember },
// });
if (response.success) {
dispatch(setUpdateAnnouncement(!update))
Toast.show({ type: 'small', text1: 'Berhasil menambahkan data', })

View File

@@ -38,7 +38,8 @@ export default function EditAnnouncement() {
const [disableBtn, setDisableBtn] = useState(true);
const [dataMember, setDataMember] = useState<any>([]);
const [fileForm, setFileForm] = useState<any[]>([])
const [indexDelFile, setIndexDelFile] = useState<number>(0)
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({
@@ -73,6 +74,7 @@ export default function EditAnnouncement() {
arrNew.push(newObject)
})
setDataMember(arrNew);
setDataFile(response.file);
} catch (error) {
console.error(error);
}
@@ -119,9 +121,22 @@ export default function EditAnnouncement() {
try {
setLoading(true)
const hasil = await decryptToken(String(token?.current))
const response = await apiEditAnnouncement({
...dataForm, user: hasil, groups: dataMember,
}, id);
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', })
@@ -148,8 +163,18 @@ export default function EditAnnouncement() {
}
};
function deleteFile(index: number) {
setFileForm([...fileForm.filter((val, i) => i !== index)])
function deleteFile(index: number | string, cat: "newFile" | "oldFile" | null) {
if (cat == "newFile") {
setFileForm([...fileForm.filter((val, i) => i !== index)])
} else {
setDataFile(prev =>
prev.map(item =>
item.id === index
? { ...item, delete: true }
: item
)
);
}
setModalFile(false)
}
@@ -207,19 +232,31 @@ export default function EditAnnouncement() {
/>
<ButtonSelect value="Upload File" onPress={pickDocumentAsync} />
{
fileForm.length > 0
(fileForm.length > 0 || dataFile.filter((val) => !val.delete).length > 0)
&&
<View style={[Styles.borderAll, Styles.round10, Styles.p10, Styles.mb10]}>
<Text style={[Styles.textDefaultSemiBold]}>File</Text>
{
dataFile.filter((val) => !val.delete).map((item, index) => (
<BorderBottomItem
key={index}
borderType={(fileForm.length + dataFile.length) > 1 ? "bottom" : "none"}
icon={<MaterialCommunityIcons name="file-outline" size={25} color="black" />}
title={item.name + '.' + item.extension}
titleWeight="normal"
onPress={() => { setIndexDelFile({ id: item.id, cat: "oldFile" }); setModalFile(true) }}
/>
))
}
{
fileForm.map((item, index) => (
<BorderBottomItem
key={index}
borderType={fileForm.length > 1 ? "bottom" : "none"}
borderType={(fileForm.length + dataFile.length) > 1 ? "bottom" : "none"}
icon={<MaterialCommunityIcons name="file-outline" size={25} color="black" />}
title={item.name}
titleWeight="normal"
onPress={() => { setIndexDelFile(index); setModalFile(true) }}
onPress={() => { setIndexDelFile({ id: index, cat: "newFile" }); setModalFile(true) }}
/>
))
}
@@ -275,7 +312,7 @@ export default function EditAnnouncement() {
<MenuItemRow
icon={<Ionicons name="trash" color="black" size={25} />}
title="Hapus"
onPress={() => { deleteFile(indexDelFile) }}
onPress={() => { deleteFile(indexDelFile.id, indexDelFile.cat) }}
/>
</View>
</DrawerBottom>

View File

@@ -23,7 +23,7 @@ import { ref } from '@react-native-firebase/database';
import { useHeaderHeight } from '@react-navigation/elements';
import { router, Stack, useLocalSearchParams } from "expo-router";
import { useEffect, useState } from "react";
import { KeyboardAvoidingView, Platform, Pressable, ScrollView, View } from "react-native";
import { KeyboardAvoidingView, Platform, Pressable, RefreshControl, ScrollView, View } from "react-native";
import Toast from "react-native-toast-message";
import { useSelector } from "react-redux";
@@ -47,6 +47,13 @@ type PropsKomentar = {
updatedAt: string
}
type PropsFile = {
id: string;
idStorage: string;
name: string;
extension: string
}
export default function DetailDiscussionGeneral() {
const { token, decryptToken } = useAuthSession()
const entityUser = useSelector((state: any) => state.user)
@@ -55,6 +62,7 @@ export default function DetailDiscussionGeneral() {
const [data, setData] = useState<Props>()
const [dataKomentar, setDataKomentar] = useState<PropsKomentar[]>([])
const [memberDiscussion, setMemberDiscussion] = useState(false)
const [fileDiscussion, setFileDiscussion] = useState<PropsFile[]>([])
const [komentar, setKomentar] = useState('')
const update = useSelector((state: any) => state.discussionGeneralDetailUpdate)
const [loading, setLoading] = useState(true)
@@ -65,6 +73,7 @@ export default function DetailDiscussionGeneral() {
const [detailMore, setDetailMore] = useState<any>([])
const [loadingSendKomentar, setLoadingSendKomentar] = useState(false)
const [isVisible, setVisible] = useState(false)
const [refreshing, setRefreshing] = useState(false)
const [selectKomentar, setSelectKomentar] = useState({
id: '',
comment: ''
@@ -93,7 +102,7 @@ export default function DetailDiscussionGeneral() {
}
async function handleLoad(cat: 'detail' | 'komentar' | 'cek-anggota', loading: boolean) {
async function handleLoad(cat: 'detail' | 'komentar' | 'cek-anggota' | 'file', loading: boolean) {
try {
if (cat == "detail") {
setLoading(loading)
@@ -110,6 +119,8 @@ export default function DetailDiscussionGeneral() {
setDataKomentar(response.data)
} else if (cat == 'cek-anggota') {
setMemberDiscussion(response.data)
} else if (cat == 'file') {
setFileDiscussion(response.data)
}
} catch (error) {
@@ -130,6 +141,7 @@ export default function DetailDiscussionGeneral() {
handleLoad('detail', true)
handleLoad('komentar', true)
handleLoad('cek-anggota', true)
handleLoad('file', true)
}, []);
async function handleKomentar() {
@@ -198,6 +210,16 @@ export default function DetailDiscussionGeneral() {
setViewEdit(!viewEdit)
}
const handleRefresh = async () => {
setRefreshing(true)
handleLoad('detail', false)
handleLoad('komentar', false)
handleLoad('cek-anggota', false)
handleLoad('file', false)
await new Promise(resolve => setTimeout(resolve, 2000));
setRefreshing(false)
};
return (
<>
<Stack.Screen
@@ -209,13 +231,23 @@ export default function DetailDiscussionGeneral() {
}}
/>
<View style={{ flex: 1 }}>
<ScrollView showsVerticalScrollIndicator={false}>
<View style={[Styles.p15, Styles.mb100]}>
<ScrollView
showsVerticalScrollIndicator={false}
style={[Styles.h100]}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={() => handleRefresh()}
/>
}
>
<View style={[Styles.p15]}>
{
loading ?
<SkeletonContent />
:
<BorderBottomItem2
dataFile={fileDiscussion}
descEllipsize={false}
borderType="bottom"
icon={

View File

@@ -125,9 +125,26 @@ export default function CreateDiscussionGeneral() {
try {
setLoading(true)
const hasil = await decryptToken(String(token?.current))
const response = await apiCreateDiscussionGeneral({
data: { ...dataForm, user: hasil, member: entitiesMember },
})
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, member: entitiesMember }
))
const response = await apiCreateDiscussionGeneral(fd)
// const response = await apiCreateDiscussionGeneral({
// data: { ...dataForm, user: hasil, member: entitiesMember },
// })
if (response.success) {
dispatch(setMemberChoose([]))
dispatch(setUpdateDiscussionGeneralDetail(!update))

View File

@@ -26,7 +26,8 @@ export default function EditDiscussionGeneral() {
const [loading, setLoading] = useState(false)
const [fileForm, setFileForm] = useState<any[]>([])
const [isModalFile, setModalFile] = useState(false)
const [indexDelFile, setIndexDelFile] = useState<number>(0)
const [indexDelFile, setIndexDelFile] = useState<{ id: string | number; cat: "newFile" | "oldFile" }>({ id: "", cat: "newFile" })
const [dataFile, setDataFile] = useState<{ id: string; idStorage: string; name: string; extension: string; delete?: boolean }[]>([])
const update = useSelector((state: any) => state.discussionGeneralDetailUpdate)
const [dataForm, setDataForm] = useState({
title: "",
@@ -45,9 +46,17 @@ export default function EditDiscussionGeneral() {
user: hasil,
cat: "detail",
});
const responseFile = await apiGetDiscussionGeneralOne({
id: id,
user: hasil,
cat: "file",
});
if (response.success) {
setDataForm(response.data);
}
if (responseFile.success) {
setDataFile(responseFile.data);
}
} catch (error) {
console.error(error);
}
@@ -102,8 +111,18 @@ export default function EditDiscussionGeneral() {
}
};
function deleteFile(index: number) {
setFileForm([...fileForm.filter((val, i) => i !== index)])
function deleteFile(index: number | string, cat: "newFile" | "oldFile" | null) {
if (cat == "newFile") {
setFileForm([...fileForm.filter((val, i) => i !== index)])
} else {
setDataFile(prev =>
prev.map(item =>
item.id === index
? { ...item, delete: true }
: item
)
);
}
setModalFile(false)
}
@@ -112,7 +131,22 @@ export default function EditDiscussionGeneral() {
try {
setLoading(true)
const hasil = await decryptToken(String(token?.current));
const response = await apiEditDiscussionGeneral({ user: hasil, title: dataForm.title, desc: dataForm.desc }, id);
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, title: dataForm.title, desc: dataForm.desc, oldFile: dataFile
}
))
const response = await apiEditDiscussionGeneral(fd, id);
if (response.success) {
dispatch(setUpdateDiscussionGeneralDetail(!update))
Toast.show({ type: 'small', text1: 'Berhasil mengubah data', })
@@ -173,19 +207,31 @@ export default function EditDiscussionGeneral() {
/>
<ButtonSelect value="Upload File" onPress={pickDocumentAsync} />
{
fileForm.length > 0
(fileForm.length > 0 || dataFile.filter((val) => !val.delete).length > 0)
&&
<View style={[Styles.borderAll, Styles.round10, Styles.p10, Styles.mb10]}>
<Text style={[Styles.textDefaultSemiBold]}>File</Text>
{
dataFile.filter((val) => !val.delete).map((item, index) => (
<BorderBottomItem
key={index}
borderType={(fileForm.length + dataFile.length) > 1 ? "bottom" : "none"}
icon={<MaterialCommunityIcons name="file-outline" size={25} color="black" />}
title={item.name + '.' + item.extension}
titleWeight="normal"
onPress={() => { setIndexDelFile({ id: item.id, cat: "oldFile" }); setModalFile(true) }}
/>
))
}
{
fileForm.map((item, index) => (
<BorderBottomItem
key={index}
borderType={fileForm.length > 1 ? "bottom" : "none"}
borderType={(fileForm.length + dataFile.length) > 1 ? "bottom" : "none"}
icon={<MaterialCommunityIcons name="file-outline" size={25} color="black" />}
title={item.name}
titleWeight="normal"
onPress={() => { setIndexDelFile(index); setModalFile(true) }}
onPress={() => { setIndexDelFile({ id: index, cat: "newFile" }); setModalFile(true) }}
/>
))
}
@@ -199,7 +245,7 @@ export default function EditDiscussionGeneral() {
<MenuItemRow
icon={<Ionicons name="trash" color="black" size={25} />}
title="Hapus"
onPress={() => { deleteFile(indexDelFile) }}
onPress={() => { deleteFile(indexDelFile.id, indexDelFile.cat) }}
/>
</View>
</DrawerBottom>

View File

@@ -1,11 +1,17 @@
import { ColorsStatus } from "@/constants/ColorsStatus";
import { ConstEnv } from "@/constants/ConstEnv";
import Styles from "@/constants/Styles";
import { Ionicons } from "@expo/vector-icons";
import * as FileSystem from 'expo-file-system';
import { startActivityAsync } from 'expo-intent-launcher';
import * as Sharing from 'expo-sharing';
import React, { useState } from "react";
import { Dimensions, Pressable, View } from "react-native";
import { Alert, Dimensions, Platform, Pressable, View } from "react-native";
import { ScrollView } from "react-native-gesture-handler";
import * as mime from 'react-native-mime-types';
import Text from "./Text";
type Props = {
title?: string
subtitle?: string | React.ReactNode
@@ -24,13 +30,47 @@ type Props = {
textColor?: string,
colorPress?: boolean
titleShowAll?: boolean
dataFile: { id: string; idStorage: string; name: string; extension: string }[]
}
export default function BorderBottomItem2({ title, subtitle, icon, desc, onPress, onLongPress, rightTopInfo, borderType, leftBottomInfo, rightBottomInfo, titleWeight, bgColor, width, descEllipsize, textColor, colorPress, titleShowAll }: Props) {
export default function BorderBottomItem2({ title, subtitle, icon, desc, onPress, onLongPress, rightTopInfo, borderType, leftBottomInfo, rightBottomInfo, titleWeight, bgColor, width, descEllipsize, textColor, colorPress, titleShowAll, dataFile }: Props) {
const lebarDim = Dimensions.get("window").width;
const lebar = width ? lebarDim * width / 100 : 'auto';
const textColorFix = textColor ? textColor : 'black';
const [isTap, setIsTap] = useState(false);
const [loadingOpen, setLoadingOpen] = useState(false)
const openFile = (item: { idStorage: string; name: string; extension: string }) => {
if (Platform.OS == 'android') setLoadingOpen(true)
let remoteUrl = ConstEnv.url_storage + '/files/' + item.idStorage;
const fileName = item.name + '.' + item.extension;
let localPath = `${FileSystem.documentDirectory}/${fileName}`;
const mimeType = mime.lookup(fileName)
FileSystem.downloadAsync(remoteUrl, localPath).then(async ({ uri }) => {
const contentURL = await FileSystem.getContentUriAsync(uri);
setLoadingOpen(false)
try {
if (Platform.OS == 'android') {
await startActivityAsync(
'android.intent.action.VIEW',
{
data: contentURL,
flags: 1,
type: mimeType as string,
}
);
} else if (Platform.OS == 'ios') {
Sharing.shareAsync(localPath);
}
} catch (error) {
Alert.alert('INFO', 'Gagal membuka file, tidak ada aplikasi yang dapat membuka file ini');
} finally {
if (Platform.OS == 'android') setLoadingOpen(false)
}
});
};
return (
@@ -69,24 +109,22 @@ export default function BorderBottomItem2({ title, subtitle, icon, desc, onPress
</View>
{desc && <Text style={[Styles.textDefault, Styles.mt05, { textAlign: 'left', color: textColorFix }]} numberOfLines={descEllipsize == false ? 0 : 2} ellipsizeMode='tail'>{desc}</Text>}
<ScrollView horizontal style={[Styles.mv05]} showsHorizontalScrollIndicator={false}>
<View style={[Styles.rowItemsCenter, Styles.borderAll, Styles.round10, Styles.ph05, Styles.pv03, Styles.mr05]}>
<Ionicons name="chatbox-ellipses-outline" size={18} color="grey" style={Styles.mr05} />
<Text style={[Styles.textInformation, Styles.cGray, Styles.mb05]}>Text petama.pdf</Text>
</View>
<View style={[Styles.rowItemsCenter, Styles.borderAll, Styles.round10, Styles.ph05, Styles.pv03, Styles.mr05]}>
<Ionicons name="chatbox-ellipses-outline" size={18} color="grey" style={Styles.mr05} />
<Text style={[Styles.textInformation, Styles.cGray, Styles.mb05]}>Text petama.pdf</Text>
</View>
<View style={[Styles.rowItemsCenter, Styles.borderAll, Styles.round10, Styles.ph05, Styles.pv03, Styles.mr05]}>
<Ionicons name="chatbox-ellipses-outline" size={18} color="grey" style={Styles.mr05} />
<Text style={[Styles.textInformation, Styles.cGray, Styles.mb05]}>Text petama.pdf</Text>
</View>
<View style={[Styles.rowItemsCenter, Styles.borderAll, Styles.round10, Styles.ph05, Styles.pv03, Styles.mr05]}>
<Ionicons name="chatbox-ellipses-outline" size={18} color="grey" style={Styles.mr05} />
<Text style={[Styles.textInformation, Styles.cGray, Styles.mb05]}>Text petama.pdf</Text>
</View>
</ScrollView>
{
dataFile.length > 0 && (
<ScrollView horizontal style={[Styles.mv05]} showsHorizontalScrollIndicator={false}>
{dataFile.map((item, index) => (
<Pressable
key={index}
style={[Styles.rowItemsCenter, Styles.borderAll, Styles.round10, Styles.ph05, Styles.pv03, Styles.mr05]}
onPress={() => { openFile({ idStorage: item.idStorage, name: item.name, extension: item.extension }) }}
>
<Ionicons name="document-text" size={18} color="grey" style={Styles.mr05} />
<Text style={[Styles.textInformation, Styles.cGray, Styles.mb05]}>{item.name}.{item.extension}</Text>
</Pressable>
))}
</ScrollView>
)
}
{
(leftBottomInfo || rightBottomInfo) &&
(

View File

@@ -230,13 +230,32 @@ export const apiDeleteDiscussionGeneral = async (data: { user: string, active: b
});
};
export const apiEditDiscussionGeneral = async (data: { user: string, title: string, desc: string }, id: string) => {
const response = await api.put(`/mobile/discussion-general/${id}`, data)
// export const apiEditDiscussionGeneral = async (data: { user: string, title: string, desc: string }, id: string) => {
// const response = await api.put(`/mobile/discussion-general/${id}`, data)
// return response.data;
// };
export const apiEditDiscussionGeneral = async (data: FormData, id: string) => {
const response = await api.put(`/mobile/discussion-general/${id}`, data, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
return response.data;
};
export const apiCreateDiscussionGeneral = async ({ data }: { data: { idGroup: string, title: string, desc: string, user: string, member: [] } }) => {
const response = await api.post(`/mobile/discussion-general`, data)
// export const apiCreateDiscussionGeneral = async ({ data }: { data: { idGroup: string, title: string, desc: string, user: string, member: [] } }) => {
// const response = await api.post(`/mobile/discussion-general`, data)
// return response.data;
// };
export const apiCreateDiscussionGeneral = async (data: FormData) => {
// const response = await api.post(`/mobile/discussion-general`, data)
const response = await api.post(`/mobile/discussion-general`, data, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
return response.data;
};
@@ -256,21 +275,41 @@ export const apiGetDivisionGroup = async ({ user }: { user: string }) => {
return response.data;
};
export const apiCreateAnnouncement = async ({ data }: { data: { title: string, desc: string, user: string, groups: any[] } }) => {
const response = await api.post(`/mobile/announcement`, data)
// export const apiCreateAnnouncement = async ({ data }: { data: { title: string, desc: string, user: string, groups: any[] } }) => {
// const response = await api.post(`/mobile/announcement`, data)
// return response.data;
// };
export const apiCreateAnnouncement = async (data: FormData) => {
const response = await api.post(`/mobile/announcement`, data, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
return response.data;
};
export const apiGetAnnouncementOne = async ({ user, id }: { user: string, id: string }) => {
const response = await api.get(`mobile/announcement/${id}?user=${user}`);
return response.data;
};
export const apiEditAnnouncement = async (data: { title: string, desc: string, user: string, groups: any[] }, id: string) => {
const response = await api.put(`/mobile/announcement/${id}`, data)
// export const apiEditAnnouncement = async (data: { title: string, desc: string, user: string, groups: any[], oldFile: any[] }, id: string) => {
// const response = await api.put(`/mobile/announcement/${id}`, data)
// return response.data;
// };
export const apiEditAnnouncement = async (data: FormData, id: string) => {
const response = await api.put(`/mobile/announcement/${id}`, data, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
return response.data;
};
export const apiDeleteAnnouncement = async (data: { user: string }, id: string) => {
const response = await api.delete(`mobile/announcement/${id}`, { data })
return response.data