Compare commits

..

12 Commits

Author SHA1 Message Date
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
1a4ccc4f66 tambahan:
- Deskripsi:
- tampilan attach file pada halaman tambah diskusi umum
- tampilan attach file pada halaman update diskusi umum
- tampilan attach file pada halaman detail diskusi umum
- tampilan attach file pada halaman tambah diskusi divisi
- tampilan attach file pada halaman update diskusi divisi
- tampilan attach file pada halaman detail diskusi divisi

No Issues
2026-01-13 14:03:05 +08:00
2ba3675b3a Merge pull request 'amalia/12-jan-26' (#4) from amalia/12-jan-26 into join
Reviewed-on: http://wibugit.wibudev.com/wibu/mobile-darmasaba/pulls/4
2026-01-12 17:49:27 +08:00
ca3d0d9d19 upd: req client
Deskripsi:

 - tampilan tambah file saat tambah data pengumuman
- tampilan list file pada halaman detail pengumuman
- tampilan tambah file saat edit data pengumuman

No Issues
2026-01-12 15:52:48 +08:00
42f245f37c fix: kode otp
Deskripsi:
- fix ganti wa jenna untuk mengirim kode otp

No Issues
2026-01-12 14:12:24 +08:00
6acfcf9a54 Merge pull request 'amalia/12-nov-25' (#3) from amalia/12-nov-25 into join
Reviewed-on: http://wibugit.wibudev.com/wibu/mobile-darmasaba/pulls/3
2025-11-12 17:51:57 +08:00
2b7022ee3d upd: version app 2025-11-12 17:51:01 +08:00
ec24cb70cb fix: tampilan dtail dvisi
Deskripsi:
- jarak mb pada list tugas hari ini di halaman detail divisi

No Issues
2025-11-12 16:29:37 +08:00
5451dc092f Merge pull request 'upd: tolak nama divisi yg sama' (#2) from amalia/27-okt-25 into join
Reviewed-on: http://wibugit.wibudev.com/wibu/mobile-darmasaba/pulls/2
2025-10-27 17:45:59 +08:00
74ba2641ca upd: tolak nama divisi yg sama
Deskripsi:
- akan ditolak jika input nama divisi udah ada
- alert konfirmasi custom 1 tombol

No Issues
2025-10-27 11:31:47 +08:00
fb8a140a31 Merge pull request 'amalia/23-okt-25' (#1) from amalia/23-okt-25 into join
Reviewed-on: http://wibugit.wibudev.com/wibu/mobile-darmasaba/pulls/1
2025-10-27 11:19:43 +08:00
17 changed files with 667 additions and 41 deletions

View File

@@ -4,7 +4,7 @@ export default {
expo: {
name: "Desa+",
slug: "mobile-darmasaba",
version: "2.0.4", // Versi aplikasi (App Store)
version: "2.0.5", // Versi aplikasi (App Store)
jsEngine: "jsc",
orientation: "portrait",
icon: "./assets/images/logo-icon-small.png",
@@ -14,7 +14,7 @@ export default {
ios: {
supportsTablet: true,
bundleIdentifier: "mobiledarmasaba.app",
buildNumber: "6",
buildNumber: "7",
infoPlist: {
ITSAppUsesNonExemptEncryption: false,
CFBundleDisplayName: "Desa+"
@@ -23,7 +23,7 @@ export default {
},
android: {
package: "mobiledarmasaba.app",
versionCode: 14,
versionCode: 15,
adaptiveIcon: {
foregroundImage: "./assets/images/logo-icon-small.png",
backgroundColor: "#ffffff"
@@ -77,7 +77,8 @@ export default {
URL_OTP: process.env.URL_OTP,
URL_STORAGE: process.env.URL_STORAGE,
URL_FIREBASE_DB: process.env.URL_FIREBASE_DB,
PASS_ENC: process.env.PASS_ENC
PASS_ENC: process.env.PASS_ENC,
WA_SERVER_TOKEN: process.env.WA_SERVER_TOKEN,
}
}
};

View File

@@ -1,14 +1,20 @@
import HeaderRightAnnouncementDetail from "@/components/announcement/headerAnnouncementDetail";
import BorderBottomItem from "@/components/borderBottomItem";
import ButtonBackHeader from "@/components/buttonBackHeader";
import Skeleton from "@/components/skeleton";
import Text from '@/components/Text';
import { ConstEnv } from "@/constants/ConstEnv";
import Styles from "@/constants/Styles";
import { apiGetAnnouncementOne } from "@/lib/api";
import { useAuthSession } from "@/providers/AuthProvider";
import { Entypo, MaterialIcons } from "@expo/vector-icons";
import { Entypo, MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons";
import * as FileSystem from 'expo-file-system';
import { startActivityAsync } from 'expo-intent-launcher';
import { router, Stack, useLocalSearchParams } from "expo-router";
import * as Sharing from 'expo-sharing';
import React, { useEffect, useState } from "react";
import { Dimensions, RefreshControl, SafeAreaView, ScrollView, View } from "react-native";
import { Alert, Dimensions, Platform, RefreshControl, SafeAreaView, ScrollView, View } from "react-native";
import * as mime from 'react-native-mime-types';
import RenderHTML from 'react-native-render-html';
import Toast from "react-native-toast-message";
import { useSelector } from "react-redux";
@@ -24,12 +30,15 @@ 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<{ 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
const [loading, setLoading] = useState(true)
const arrSkeleton = Array.from({ length: 2 }, (_, index) => index)
const [refreshing, setRefreshing] = useState(false)
const [loadingOpen, setLoadingOpen] = useState(false)
async function handleLoad(loading: boolean) {
try {
@@ -39,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 })
}
@@ -70,6 +80,37 @@ export default function DetailAnnouncement() {
setRefreshing(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 (
<SafeAreaView>
<Stack.Screen
@@ -127,7 +168,26 @@ export default function DetailAnnouncement() {
}
</View>
<View style={[Styles.wrapPaper, Styles.mv15]}>
{
dataFile.length > 0 && (
<View style={[Styles.wrapPaper, Styles.mt10]}>
<View style={[Styles.mb05]}>
<Text style={[Styles.textDefaultSemiBold]}>File</Text>
</View>
{dataFile.map((item, index) => (
<BorderBottomItem
key={index}
borderType="bottom"
icon={<MaterialCommunityIcons name="file-outline" size={25} color="black" />}
title={item.name}
titleWeight="normal"
onPress={() => { openFile({ idStorage: item.idStorage, name: item.name, extension: item.extension }) }}
/>
))}
</View>
)
}
<View style={[Styles.wrapPaper, Styles.mt10]}>
{
loading ?
arrSkeleton.map((item, index) => {

View File

@@ -1,14 +1,18 @@
import BorderBottomItem from "@/components/borderBottomItem";
import ButtonBackHeader from "@/components/buttonBackHeader";
import ButtonSaveHeader from "@/components/buttonSaveHeader";
import ButtonSelect from "@/components/buttonSelect";
import DrawerBottom from "@/components/drawerBottom";
import { InputForm } from "@/components/inputForm";
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 { Entypo } from "@expo/vector-icons";
import { Entypo, Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
import * as DocumentPicker from "expo-document-picker";
import { router, Stack } from "expo-router";
import { useEffect, useState } from "react";
import { SafeAreaView, ScrollView, StyleSheet, View } from "react-native";
@@ -23,6 +27,9 @@ export default function CreateAnnouncement() {
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: "",
@@ -69,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', })
@@ -84,6 +108,25 @@ export default function CreateAnnouncement() {
}
}
const pickDocumentAsync = async () => {
let result = await DocumentPicker.getDocumentAsync({
type: ["*/*"],
multiple: true
});
if (!result.canceled) {
for (let i = 0; i < result.assets?.length; i++) {
if (result.assets[i].uri) {
setFileForm((prev) => [...prev, result.assets[i]])
}
}
}
};
function deleteFile(index: number) {
setFileForm([...fileForm.filter((val, i) => i !== index)])
setModalFile(false)
}
return (
<SafeAreaView>
<Stack.Screen
@@ -134,6 +177,27 @@ export default function CreateAnnouncement() {
onChange={(val) => validationForm("desc", val)}
multiline
/>
<ButtonSelect value="Upload File" onPress={pickDocumentAsync} />
{
fileForm.length > 0
&&
<View style={[Styles.borderAll, Styles.round10, Styles.p10, Styles.mb10]}>
<Text style={[Styles.textDefaultSemiBold]}>File</Text>
{
fileForm.map((item, index) => (
<BorderBottomItem
key={index}
borderType={fileForm.length > 1 ? "bottom" : "none"}
icon={<MaterialCommunityIcons name="file-outline" size={25} color="black" />}
title={item.name}
titleWeight="normal"
onPress={() => { setIndexDelFile(index); setModalFile(true) }}
/>
))
}
</View>
}
<ButtonSelect
value="Pilih divisi penerima pengumuman"
onPress={() => {
@@ -178,6 +242,16 @@ export default function CreateAnnouncement() {
setModalDivisi(false)
}}
/>
<DrawerBottom animation="slide" isVisible={isModalFile} setVisible={setModalFile} title="Menu">
<View style={Styles.rowItemsCenter}>
<MenuItemRow
icon={<Ionicons name="trash" color="black" size={25} />}
title="Hapus"
onPress={() => { deleteFile(indexDelFile) }}
/>
</View>
</DrawerBottom>
</SafeAreaView>
);
}

View File

@@ -1,14 +1,18 @@
import BorderBottomItem from "@/components/borderBottomItem";
import ButtonBackHeader from "@/components/buttonBackHeader";
import ButtonSaveHeader from "@/components/buttonSaveHeader";
import ButtonSelect from "@/components/buttonSelect";
import DrawerBottom from "@/components/drawerBottom";
import { InputForm } from "@/components/inputForm";
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 { Entypo } from "@expo/vector-icons";
import { Entypo, Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
import * as DocumentPicker from "expo-document-picker";
import { router, Stack, useLocalSearchParams } from "expo-router";
import { useEffect, useState } from "react";
import { SafeAreaView, ScrollView, View } from "react-native";
@@ -33,6 +37,10 @@ export default function EditAnnouncement() {
const [modalDivisi, setModalDivisi] = useState(false);
const [disableBtn, setDisableBtn] = useState(true);
const [dataMember, setDataMember] = useState<any>([]);
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: "",
@@ -66,6 +74,7 @@ export default function EditAnnouncement() {
arrNew.push(newObject)
})
setDataMember(arrNew);
setDataFile(response.file);
} catch (error) {
console.error(error);
}
@@ -112,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', })
@@ -127,6 +149,35 @@ export default function EditAnnouncement() {
}
}
const pickDocumentAsync = async () => {
let result = await DocumentPicker.getDocumentAsync({
type: ["*/*"],
multiple: true
});
if (!result.canceled) {
for (let i = 0; i < result.assets?.length; i++) {
if (result.assets[i].uri) {
setFileForm((prev) => [...prev, result.assets[i]])
}
}
}
};
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)
}
return (
<SafeAreaView>
<Stack.Screen
@@ -179,6 +230,38 @@ export default function EditAnnouncement() {
value={dataForm.desc}
multiline
/>
<ButtonSelect value="Upload File" onPress={pickDocumentAsync} />
{
(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 + dataFile.length) > 1 ? "bottom" : "none"}
icon={<MaterialCommunityIcons name="file-outline" size={25} color="black" />}
title={item.name}
titleWeight="normal"
onPress={() => { setIndexDelFile({ id: index, cat: "newFile" }); setModalFile(true) }}
/>
))
}
</View>
}
<ButtonSelect
value="Pilih divisi penerima pengumuman"
onPress={() => {
@@ -223,6 +306,16 @@ export default function EditAnnouncement() {
}}
value={dataMember}
/>
<DrawerBottom animation="slide" isVisible={isModalFile} setVisible={setModalFile} title="Menu">
<View style={Styles.rowItemsCenter}>
<MenuItemRow
icon={<Ionicons name="trash" color="black" size={25} />}
title="Hapus"
onPress={() => { deleteFile(indexDelFile.id, indexDelFile.cat) }}
/>
</View>
</DrawerBottom>
</SafeAreaView>
);
}

View File

@@ -1,5 +1,6 @@
import AlertKonfirmasi from "@/components/alertKonfirmasi";
import BorderBottomItem from "@/components/borderBottomItem";
import BorderBottomItem2 from "@/components/borderBottomItem2";
import ButtonBackHeader from "@/components/buttonBackHeader";
import HeaderRightDiscussionGeneralDetail from "@/components/discussion_general/headerDiscussionDetail";
import DrawerBottom from "@/components/drawerBottom";
@@ -214,7 +215,7 @@ export default function DetailDiscussionGeneral() {
loading ?
<SkeletonContent />
:
<BorderBottomItem
<BorderBottomItem2
descEllipsize={false}
borderType="bottom"
icon={

View File

@@ -2,8 +2,10 @@ import BorderBottomItem from "@/components/borderBottomItem";
import ButtonBackHeader from "@/components/buttonBackHeader";
import ButtonSaveHeader from "@/components/buttonSaveHeader";
import ButtonSelect from "@/components/buttonSelect";
import DrawerBottom from "@/components/drawerBottom";
import ImageUser from "@/components/imageNew";
import { InputForm } from "@/components/inputForm";
import MenuItemRow from "@/components/menuItemRow";
import ModalSelect from "@/components/modalSelect";
import SelectForm from "@/components/selectForm";
import Text from '@/components/Text';
@@ -13,6 +15,8 @@ import { apiCreateDiscussionGeneral } from "@/lib/api";
import { setUpdateDiscussionGeneralDetail } from "@/lib/discussionGeneralDetail";
import { setMemberChoose } from "@/lib/memberChoose";
import { useAuthSession } from "@/providers/AuthProvider";
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
import * as DocumentPicker from "expo-document-picker";
import { router, Stack } from "expo-router";
import { useEffect, useState } from "react";
import { SafeAreaView, ScrollView, View } from "react-native";
@@ -33,6 +37,9 @@ export default function CreateDiscussionGeneral() {
const entitiesMember = useSelector((state: any) => state.memberChoose)
const update = useSelector((state: any) => state.discussionGeneralDetailUpdate)
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({
idGroup: "",
title: "",
@@ -95,6 +102,25 @@ export default function CreateDiscussionGeneral() {
router.back()
}
const pickDocumentAsync = async () => {
let result = await DocumentPicker.getDocumentAsync({
type: ["*/*"],
multiple: true
});
if (!result.canceled) {
for (let i = 0; i < result.assets?.length; i++) {
if (result.assets[i].uri) {
setFileForm((prev) => [...prev, result.assets[i]])
}
}
}
};
function deleteFile(index: number) {
setFileForm([...fileForm.filter((val, i) => i !== index)])
setModalFile(false)
}
async function handleCreate() {
try {
setLoading(true)
@@ -181,6 +207,26 @@ export default function CreateDiscussionGeneral() {
onChange={(val) => { validationForm("desc", val) }}
multiline
/>
<ButtonSelect value="Upload File" onPress={pickDocumentAsync} />
{
fileForm.length > 0
&&
<View style={[Styles.borderAll, Styles.round10, Styles.p10, Styles.mb10]}>
<Text style={[Styles.textDefaultSemiBold]}>File</Text>
{
fileForm.map((item, index) => (
<BorderBottomItem
key={index}
borderType={fileForm.length > 1 ? "bottom" : "none"}
icon={<MaterialCommunityIcons name="file-outline" size={25} color="black" />}
title={item.name}
titleWeight="normal"
onPress={() => { setIndexDelFile(index); setModalFile(true) }}
/>
))
}
</View>
}
<ButtonSelect
value="Pilih Anggota"
onPress={() => {
@@ -240,6 +286,16 @@ export default function CreateDiscussionGeneral() {
idParent={valSelect == "member" ? chooseGroup.val : ""}
valChoose={valChoose}
/>
<DrawerBottom animation="slide" isVisible={isModalFile} setVisible={setModalFile} title="Menu">
<View style={Styles.rowItemsCenter}>
<MenuItemRow
icon={<Ionicons name="trash" color="black" size={25} />}
title="Hapus"
onPress={() => { deleteFile(indexDelFile) }}
/>
</View>
</DrawerBottom>
</SafeAreaView>
);
}

View File

@@ -1,10 +1,17 @@
import Text from "@/components/Text";
import BorderBottomItem from "@/components/borderBottomItem";
import ButtonBackHeader from "@/components/buttonBackHeader";
import ButtonSaveHeader from "@/components/buttonSaveHeader";
import ButtonSelect from "@/components/buttonSelect";
import DrawerBottom from "@/components/drawerBottom";
import { InputForm } from "@/components/inputForm";
import MenuItemRow from "@/components/menuItemRow";
import Styles from "@/constants/Styles";
import { apiEditDiscussionGeneral, apiGetDiscussionGeneralOne } from "@/lib/api";
import { setUpdateDiscussionGeneralDetail } from "@/lib/discussionGeneralDetail";
import { useAuthSession } from "@/providers/AuthProvider";
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
import * as DocumentPicker from "expo-document-picker";
import { router, Stack, useLocalSearchParams } from "expo-router";
import { useEffect, useState } from "react";
import { SafeAreaView, ScrollView, View } from "react-native";
@@ -17,6 +24,9 @@ export default function EditDiscussionGeneral() {
const [disableBtn, setDisableBtn] = useState(false)
const dispatch = useDispatch()
const [loading, setLoading] = useState(false)
const [fileForm, setFileForm] = useState<any[]>([])
const [isModalFile, setModalFile] = useState(false)
const [indexDelFile, setIndexDelFile] = useState<number>(0)
const update = useSelector((state: any) => state.discussionGeneralDetailUpdate)
const [dataForm, setDataForm] = useState({
title: "",
@@ -78,6 +88,25 @@ export default function EditDiscussionGeneral() {
checkForm()
}, [error, dataForm])
const pickDocumentAsync = async () => {
let result = await DocumentPicker.getDocumentAsync({
type: ["*/*"],
multiple: true
});
if (!result.canceled) {
for (let i = 0; i < result.assets?.length; i++) {
if (result.assets[i].uri) {
setFileForm((prev) => [...prev, result.assets[i]])
}
}
}
};
function deleteFile(index: number) {
setFileForm([...fileForm.filter((val, i) => i !== index)])
setModalFile(false)
}
async function handleEdit() {
try {
@@ -142,8 +171,38 @@ export default function EditDiscussionGeneral() {
onChange={(val) => validationForm("desc", val)}
multiline
/>
<ButtonSelect value="Upload File" onPress={pickDocumentAsync} />
{
fileForm.length > 0
&&
<View style={[Styles.borderAll, Styles.round10, Styles.p10, Styles.mb10]}>
<Text style={[Styles.textDefaultSemiBold]}>File</Text>
{
fileForm.map((item, index) => (
<BorderBottomItem
key={index}
borderType={fileForm.length > 1 ? "bottom" : "none"}
icon={<MaterialCommunityIcons name="file-outline" size={25} color="black" />}
title={item.name}
titleWeight="normal"
onPress={() => { setIndexDelFile(index); setModalFile(true) }}
/>
))
}
</View>
}
</View>
</ScrollView>
<DrawerBottom animation="slide" isVisible={isModalFile} setVisible={setModalFile} title="Menu">
<View style={Styles.rowItemsCenter}>
<MenuItemRow
icon={<Ionicons name="trash" color="black" size={25} />}
title="Hapus"
onPress={() => { deleteFile(indexDelFile) }}
/>
</View>
</DrawerBottom>
</SafeAreaView>
);
}

View File

@@ -1,10 +1,17 @@
import BorderBottomItem from "@/components/borderBottomItem";
import ButtonBackHeader from "@/components/buttonBackHeader";
import ButtonSaveHeader from "@/components/buttonSaveHeader";
import ButtonSelect from "@/components/buttonSelect";
import DrawerBottom from "@/components/drawerBottom";
import { InputForm } from "@/components/inputForm";
import MenuItemRow from "@/components/menuItemRow";
import Text from "@/components/Text";
import Styles from "@/constants/Styles";
import { apiEditDiscussion, apiGetDiscussionOne } from "@/lib/api";
import { setUpdateDiscussion } from "@/lib/discussionUpdate";
import { useAuthSession } from "@/providers/AuthProvider";
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
import * as DocumentPicker from "expo-document-picker";
import { router, Stack, useLocalSearchParams } from "expo-router";
import { useEffect, useState } from "react";
import { SafeAreaView, ScrollView, View } from "react-native";
@@ -18,6 +25,9 @@ export default function DiscussionDivisionEdit() {
const update = useSelector((state: any) => state.discussionUpdate);
const dispatch = useDispatch();
const [loading, setLoading] = useState(false)
const [fileForm, setFileForm] = useState<any[]>([])
const [isModalFile, setModalFile] = useState(false)
const [indexDelFile, setIndexDelFile] = useState<number>(0)
async function handleLoad() {
try {
@@ -49,7 +59,7 @@ export default function DiscussionDivisionEdit() {
Toast.show({ type: 'small', text1: 'Berhasil mengubah data', })
dispatch(setUpdateDiscussion({ ...update, data: !update.data }));
router.back();
}else{
} else {
Toast.show({ type: 'small', text1: response.message, })
}
} catch (error) {
@@ -60,6 +70,27 @@ export default function DiscussionDivisionEdit() {
}
}
const pickDocumentAsync = async () => {
let result = await DocumentPicker.getDocumentAsync({
type: ["*/*"],
multiple: true
});
if (!result.canceled) {
for (let i = 0; i < result.assets?.length; i++) {
if (result.assets[i].uri) {
setFileForm((prev) => [...prev, result.assets[i]])
}
}
}
};
function deleteFile(index: number) {
setFileForm([...fileForm.filter((val, i) => i !== index)])
setModalFile(false)
}
return (
<SafeAreaView>
<Stack.Screen
@@ -95,8 +126,41 @@ export default function DiscussionDivisionEdit() {
onChange={setData}
multiline
/>
<ButtonSelect value="Upload File" onPress={pickDocumentAsync} />
{
fileForm.length > 0
&&
<View style={[Styles.borderAll, Styles.round10, Styles.p10, Styles.mb10]}>
<Text style={[Styles.textDefaultSemiBold]}>File</Text>
{
fileForm.map((item, index) => (
<BorderBottomItem
key={index}
borderType={fileForm.length > 1 ? "bottom" : "none"}
icon={<MaterialCommunityIcons name="file-outline" size={25} color="black" />}
title={item.name}
titleWeight="normal"
onPress={() => { setIndexDelFile(index); setModalFile(true) }}
/>
))
}
</View>
}
</View>
</ScrollView>
<DrawerBottom animation="slide" isVisible={isModalFile} setVisible={setModalFile} title="Menu">
<View style={Styles.rowItemsCenter}>
<MenuItemRow
icon={<Ionicons name="trash" color="black" size={25} />}
title="Hapus"
onPress={() => { deleteFile(indexDelFile) }}
/>
</View>
</DrawerBottom>
</SafeAreaView>
);
}

View File

@@ -1,5 +1,6 @@
import AlertKonfirmasi from "@/components/alertKonfirmasi";
import BorderBottomItem from "@/components/borderBottomItem";
import BorderBottomItem2 from "@/components/borderBottomItem2";
import ButtonBackHeader from "@/components/buttonBackHeader";
import HeaderRightDiscussionDetail from "@/components/discussion/headerDiscussionDetail";
import DrawerBottom from "@/components/drawerBottom";
@@ -288,7 +289,7 @@ export default function DiscussionDetail() {
loading ?
<SkeletonContent />
:
<BorderBottomItem
<BorderBottomItem2
descEllipsize={false}
borderType="bottom"
icon={

View File

@@ -1,16 +1,24 @@
import BorderBottomItem from "@/components/borderBottomItem"
import ButtonBackHeader from "@/components/buttonBackHeader"
import ButtonSaveHeader from "@/components/buttonSaveHeader"
import ButtonSelect from "@/components/buttonSelect"
import DrawerBottom from "@/components/drawerBottom"
import { InputForm } from "@/components/inputForm"
import MenuItemRow from "@/components/menuItemRow"
import Text from "@/components/Text"
import Styles from "@/constants/Styles"
import { apiCreateDiscussion } from "@/lib/api"
import { setUpdateDiscussion } from "@/lib/discussionUpdate"
import { useAuthSession } from "@/providers/AuthProvider"
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"
import * as DocumentPicker from "expo-document-picker"
import { router, Stack, useLocalSearchParams } from "expo-router"
import { useState } from "react"
import { SafeAreaView, ScrollView, View } from "react-native"
import Toast from "react-native-toast-message"
import { useDispatch, useSelector } from "react-redux"
export default function CreateDiscussionDivision() {
const { id } = useLocalSearchParams<{ id: string }>()
const [desc, setDesc] = useState('')
@@ -18,6 +26,29 @@ export default function CreateDiscussionDivision() {
const update = useSelector((state: any) => state.discussionUpdate)
const dispatch = useDispatch();
const [loading, setLoading] = useState(false)
const [fileForm, setFileForm] = useState<any[]>([])
const [isModalFile, setModalFile] = useState(false)
const [indexDelFile, setIndexDelFile] = useState<number>(0)
const pickDocumentAsync = async () => {
let result = await DocumentPicker.getDocumentAsync({
type: ["*/*"],
multiple: true
});
if (!result.canceled) {
for (let i = 0; i < result.assets?.length; i++) {
if (result.assets[i].uri) {
setFileForm((prev) => [...prev, result.assets[i]])
}
}
}
};
function deleteFile(index: number) {
setFileForm([...fileForm.filter((val, i) => i !== index)])
setModalFile(false)
}
async function handleCreate() {
try {
@@ -64,8 +95,38 @@ export default function CreateDiscussionDivision() {
onChange={setDesc}
multiline
/>
<ButtonSelect value="Upload File" onPress={pickDocumentAsync} />
{
fileForm.length > 0
&&
<View style={[Styles.borderAll, Styles.round10, Styles.p10, Styles.mb10]}>
<Text style={[Styles.textDefaultSemiBold]}>File</Text>
{
fileForm.map((item, index) => (
<BorderBottomItem
key={index}
borderType={fileForm.length > 1 ? "bottom" : "none"}
icon={<MaterialCommunityIcons name="file-outline" size={25} color="black" />}
title={item.name}
titleWeight="normal"
onPress={() => { setIndexDelFile(index); setModalFile(true) }}
/>
))
}
</View>
}
</View>
</ScrollView>
<DrawerBottom animation="slide" isVisible={isModalFile} setVisible={setModalFile} title="Menu">
<View style={Styles.rowItemsCenter}>
<MenuItemRow
icon={<Ionicons name="trash" color="black" size={25} />}
title="Hapus"
onPress={() => { deleteFile(indexDelFile) }}
/>
</View>
</DrawerBottom>
</SafeAreaView>
)
}

View File

@@ -69,10 +69,9 @@ export default function CreateDivision() {
if (!response.available) {
AlertKonfirmasi({
title: 'Peringatan',
desc: 'Nama divisi sudah ada. Apakah anda yakin ingin membuat divisi dengan nama yang sama?',
onPress: () => {
handleSetData()
}
category: 'warning',
desc: 'Nama divisi sudah ada. Tidak dapat membuat divisi dengan nama yang sama',
onPress: () => { }
})
} else {
handleSetData()

View File

@@ -5,17 +5,27 @@ type Props = {
title: string,
desc: string
onPress: () => void
category?: string
}
export default function AlertKonfirmasi({ title, desc, onPress }: Props) {
Alert.alert(title, desc, [
{
text: 'Tidak',
style: 'cancel',
},
{
text: 'Ya',
onPress: () => { onPress() }
},
]);
export default function AlertKonfirmasi({ title, desc, onPress, category }: Props) {
if (category == "warning") {
Alert.alert(title, desc, [
{
text: 'Oke',
style: 'cancel',
},
]);
} else {
Alert.alert(title, desc, [
{
text: 'Tidak',
style: 'cancel',
},
{
text: 'Ya',
onPress: () => { onPress() }
},
]);
}
}

View File

@@ -0,0 +1,111 @@
import { ColorsStatus } from "@/constants/ColorsStatus";
import Styles from "@/constants/Styles";
import { Ionicons } from "@expo/vector-icons";
import React, { useState } from "react";
import { Dimensions, Pressable, View } from "react-native";
import { ScrollView } from "react-native-gesture-handler";
import Text from "./Text";
type Props = {
title?: string
subtitle?: string | React.ReactNode
icon: React.ReactNode
desc?: string
rightTopInfo?: string
onPress?: () => void
onLongPress?: () => void
borderType: 'all' | 'bottom' | 'none'
leftBottomInfo?: React.ReactNode | string
rightBottomInfo?: React.ReactNode | string
titleWeight?: 'normal' | 'bold'
bgColor?: 'white' | 'transparent'
width?: number
descEllipsize?: boolean
textColor?: string,
colorPress?: boolean
titleShowAll?: boolean
}
export default function BorderBottomItem2({ title, subtitle, icon, desc, onPress, onLongPress, rightTopInfo, borderType, leftBottomInfo, rightBottomInfo, titleWeight, bgColor, width, descEllipsize, textColor, colorPress, titleShowAll }: Props) {
const lebarDim = Dimensions.get("window").width;
const lebar = width ? lebarDim * width / 100 : 'auto';
const textColorFix = textColor ? textColor : 'black';
const [isTap, setIsTap] = useState(false);
return (
<Pressable onLongPress={onLongPress} onPress={onPress}
onPressIn={() => setIsTap(true)}
onPressOut={() => setIsTap(false)}
style={({ pressed }) => [
borderType == 'bottom'
? Styles.wrapItemBorderBottom
: borderType == 'all'
? Styles.wrapItemBorderAll
: Styles.wrapItemBorderNone,
bgColor && bgColor == 'white' && ColorsStatus.white,
// efek warna saat ditekan (sementara)
isTap && colorPress && ColorsStatus.pressedGray,
]}
>
<View style={[Styles.rowItemsCenter]}>
{icon}
<View style={[Styles.rowSpaceBetween, width ? { width: lebar } : { width: '88%' }]}>
<View style={[Styles.ml10, rightTopInfo ? { width: '70%' } : { width: '90%' }]}>
<Text style={[titleWeight == 'normal' ? Styles.textDefault : Styles.textDefaultSemiBold, { color: textColorFix }]} numberOfLines={titleShowAll ? 0 : 1} ellipsizeMode='tail'>{title}</Text>
{
subtitle &&
typeof subtitle == "string"
? <Text style={[Styles.textMediumNormal, { lineHeight: 15, color: textColorFix }]}>{subtitle}</Text>
: <View style={{ alignItems: 'flex-start' }}>
{subtitle}
</View>
}
</View>
{
rightTopInfo && <Text style={[Styles.textInformation, Styles.mt05, { color: textColorFix }]}>{rightTopInfo}</Text>
}
</View>
</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>
{
(leftBottomInfo || rightBottomInfo) &&
(
<View style={[rightBottomInfo && !leftBottomInfo ? Styles.rowSpaceBetweenReverse : Styles.rowSpaceBetween, Styles.mt05]}>
{
typeof leftBottomInfo == 'string' ?
<Text style={[Styles.textInformation, Styles.cGray]}>{leftBottomInfo}</Text>
:
leftBottomInfo
}
{
typeof rightBottomInfo == 'string' ?
<Text style={[Styles.textInformation, Styles.cGray]}>{rightBottomInfo}</Text>
:
rightBottomInfo
}
</View>
)
}
</Pressable>
)
}

View File

@@ -57,7 +57,7 @@ export default function TaskDivisionDetail({ refreshing }: { refreshing: boolean
:
data.length > 0 ?
data.map((item, index) => (
<Pressable key={index} style={[Styles.wrapPaper]} onPress={() => { router.push(`/division/${id}/task/${item.idProject}`) }}>
<Pressable key={index} style={[Styles.wrapPaper, Styles.mb05]} onPress={() => { router.push(`/division/${id}/task/${item.idProject}`) }}>
<Text style={[Styles.textDefaultSemiBold]} numberOfLines={1} ellipsizeMode="tail">{item.title} - {item.projectTitle}</Text>
<View style={[Styles.rowItemsCenter, Styles.mt10]}>
<Feather name="clock" size={18} color="grey" style={Styles.mr05} />

View File

@@ -178,6 +178,9 @@ const Styles = StyleSheet.create({
ph20: {
paddingHorizontal: 20,
},
pv03: {
paddingVertical: 3
},
pv05: {
paddingVertical: 5
},

View File

@@ -19,7 +19,7 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>2.0.4</string>
<string>2.0.5</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
@@ -39,7 +39,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>6</string>
<string>7</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSMinimumSystemVersion</key>

View File

@@ -11,7 +11,20 @@ export const apiCheckPhoneLogin = async (body: { phone: string }) => {
}
export const apiSendOtp = async (body: { phone: string, otp: number }) => {
const res = await axios.get(`${Constants.expoConfig?.extra?.URL_OTP}/code?nom=${body.phone}&text=*Desa%2B*%0AMasukkan%20kode%20ini%20*${encodeURIComponent(body.otp)}*%20pada%20aplikasi%20Desa%2B%20anda.%20Jangan%20berikan%20pada%20siapapun.`)
const message = "Desa+\nMasukkan kode ini " + body.otp + " pada aplikasi Desa+ anda. Jangan berikan pada siapapun."
const textFix = encodeURIComponent(message)
// const res = await axios.get(`${Constants.expoConfig?.extra?.URL_OTP}/code?nom=${body.phone}&text=*Desa%2B*%0AMasukkan%20kode%20ini%20*${encodeURIComponent(body.otp)}*%20pada%20aplikasi%20Desa%2B%20anda.%20Jangan%20berikan%20pada%20siapapun.`)
const res = await fetch(
`${Constants.expoConfig?.extra?.URL_OTP}/code?nom=${body.phone}&text=${textFix}`,
{
cache: "no-cache",
headers: {
Authorization: `Bearer ${Constants.expoConfig?.extra?.WA_SERVER_TOKEN}`,
},
}
);
return res.status
}
@@ -243,21 +256,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