Compare commits

...

59 Commits

Author SHA1 Message Date
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
f341bd01c6 update 2025-10-27 10:48:50 +08:00
c458504da2 upd: tampilan
Deskripsi:
- list diskusi pda dashboard dan detail divisi home
- ukuran banner dan stretch
- detail anggota > nama
- detail diskusi umum > judul kepotong
- detail pengumuman > deskripsi tidak keliatan warna putih

No Issues
2025-10-23 13:17:42 +08:00
a0d1b90662 upd: version update 2025-10-22 17:29:17 +08:00
bd9fe8676d upd : package ga terpakai 2025-10-22 17:26:33 +08:00
56856a96fd upd version android 2025-10-16 17:42:58 +08:00
049f6c63cc fix: detail pengumuman jika data kosong 2025-10-16 15:20:53 +08:00
2ea92d3e9a upd: version app ios dan android 2025-10-15 16:52:32 +08:00
04f7bda40f upd: tombol style
Deskripsi:
- bgcolor saat long press

No Issues
2025-10-15 15:41:42 +08:00
863871aae5 Merge pull request 'upd: aksi komentar diskusi' (#51) from amalia/14-okt-25 into join
Reviewed-on: bip/mobile-darmasaba#51
2025-10-14 17:35:47 +08:00
0ce1f270ef upd: aksi komentar diskusi
Deskripsi:
- api hapus komentar diskusi umum dan diskusi divisi
- api edit komentar diskusi umum dan diskusi divisi
- layout edit komentar diskusi umum dan diskusi divisi
- pengaplikasian edit komentar pada diskusi umum dan diskusi divisi
- pengaplikasian hapus komentar pada diskusi umum dan diskudi divisi

No Issues
2025-10-14 14:58:54 +08:00
6ffda375a4 Merge pull request 'amalia/13-okt-25' (#50) from amalia/13-okt-25 into join
Reviewed-on: bip/mobile-darmasaba#50
2025-10-13 17:20:53 +08:00
2347b322cf upd : tampilan edit komentar
Deskripsi:
- menampilkan tulisan edited jika komentar telah di edit
- menampilkan modal jika komentar di longpress

No Issues
2025-10-13 17:17:57 +08:00
13a1d0e858 fix: tinggi drawer
Deskripsi:
- update tinggi drawer bottom pada detail project dan detail tugas divisi karena tombol batal tertutup navigasi pada device android

No Issues
2025-10-13 16:08:12 +08:00
f6ea4f65bb Merge pull request 'amalia/09-okt-25' (#49) from amalia/09-okt-25 into join
Reviewed-on: bip/mobile-darmasaba#49
2025-10-09 17:55:18 +08:00
6069756d6f fix :komentar diskusi
Deskripsi:
- list komentar bisa di lihat lebih banyak >> diskusi umum dan diskusi divisi
- loading disable saat menambah komentar >> diskusi umum

No Issues
2025-10-09 14:21:14 +08:00
5de8962a0a upd: rollout new version 2025-10-09 10:12:18 +08:00
67ac6d920c Merge pull request 'upd: komentar diskusi umum dan divisi' (#48) from amalia/08-okt-25 into join
Reviewed-on: bip/mobile-darmasaba#48
2025-10-08 14:08:05 +08:00
f9c8c92d3b upd: komentar diskusi umum dan divisi
Deskripsi:
- memberikan note sesuai dengan status diskusi agar lebih jelas
- pada fitur diskusi umum dan diskusi divisi

No Issues
2025-10-08 12:07:56 +08:00
9b18322f38 Merge pull request 'amalia/07-okt-25' (#47) from amalia/07-okt-25 into join
Reviewed-on: bip/mobile-darmasaba#47
2025-10-07 17:42:13 +08:00
af24a8af23 upd: tambah komentar
Deskripsi:
- mendeteksi jika yg diinputkan hanya spasi atau enter maka tidak bisa mengirim komentar
- diskusi umum dan diskusi divisi

No Issues
2025-10-07 16:51:57 +08:00
95121d0442 upd: order list tugas
Deskripsi:
- mengurutkan data tugas saat membuat data tugas divisi dan kegiatan

No Issues
2025-10-07 15:02:24 +08:00
5e1ed12ca8 upd: nama divisi
Deskripsi:
- update check nama divisi ketika ada yg sama pada 1 group dan desa

No Issuese
2025-10-07 11:59:24 +08:00
9dde198d5e fix: scroll down
Deskripsi:
- safeareaview dihapus dan menambahkan flex 1 agar bisa scroll down
- tugas divisi dan kegiatan

No Issues
2025-10-07 11:17:40 +08:00
5fcabc5d77 fix: scroll
Deskripsi:
- scrool down saat menambahkan anggota dan memilih admin pada saat tambah divisi

No Issues
2025-10-07 11:02:47 +08:00
9fa19af68b Merge pull request 'amalia/06-okt-25' (#46) from amalia/06-okt-25 into join
Reviewed-on: bip/mobile-darmasaba#46
2025-10-06 17:25:13 +08:00
d2cb7d7738 upd: warna status bar
deskripsi:
- warna status bar pada login, halaman kode otp, dan file layout aplikasi

No Issues
2025-10-06 14:30:07 +08:00
a27c6181dd fix : list kalendar
Deskripsi:
- nama user kepotong pada list kalendar

No Issues
2025-10-06 14:06:03 +08:00
c4e48726e0 Merge pull request 'amalia/03-okt-25' (#45) from amalia/03-okt-25 into join
Reviewed-on: bip/mobile-darmasaba#45
2025-10-03 17:31:32 +08:00
ab4813d3aa upd: upload image
Deskripsi:
- upload image edit crop

NO Issues
2025-10-03 17:30:22 +08:00
60278fee16 upd: edi photo
Deskripsi
- edit image before upload pada edit profile, tambah anggota dan edit anggota

No Issues
2025-10-03 14:08:22 +08:00
10d4c94cc1 upd: validasi nama pengguna
Deskripsi:
- validasi nama pada edit profile, tambah anggota, edit anggota

NO Issues
2025-10-03 11:52:03 +08:00
78e7323eab fix: input tanggal dan jam
deskripsi:
- fix input date dan time pada ios theme

NO Issues
2025-10-03 11:17:01 +08:00
ea1c0bd67e upd: refresh pada halaman informasi divisi 2025-10-03 10:39:56 +08:00
1698cc703c fix: tambah anggota
Deskripsi:
- disable false saat tambah anggota dan loading false

No Issues
2025-10-03 10:35:51 +08:00
43362da45a upd: prebuild build 2025-10-01 10:44:54 +08:00
34d727f07d Merge pull request 'upd: android input' (#44) from amalia/29-sept-25 into join
Reviewed-on: bip/mobile-darmasaba#44
2025-09-29 17:35:24 +08:00
ed175d63f2 upd: android input
Deskripsi:
- update input komentar pada android

No Issues
2025-09-29 15:14:54 +08:00
bd82b7c427 Merge pull request 'amalia/26-sept-25' (#43) from amalia/26-sept-25 into join
Reviewed-on: bip/mobile-darmasaba#43
2025-09-26 17:42:28 +08:00
a6c96105d2 upd: text input komentar
Deskripsi:
- android input komentar pada android

No Issues
2025-09-26 16:52:35 +08:00
14e9bf15c7 upd: dokumen divisi
Deskripsi:
- update akses role pada dokumen divisi

No Issues
2025-09-26 15:08:59 +08:00
907b56feaf upd: text input
Deskripsi:
- text input pada android

No Issues
2025-09-26 12:22:12 +08:00
2341a46992 upd: task divisi
Deskripsi:
- user role akses

No Issues
2025-09-26 11:03:55 +08:00
43a91c6481 Merge pull request 'amalia/25-sept-25' (#42) from amalia/25-sept-25 into join
Reviewed-on: bip/mobile-darmasaba#42
2025-09-26 10:22:11 +08:00
ecc41c905f upd 2025-09-25 17:13:44 +08:00
65d53951c3 upd: caraousel kegiatan home
Deskripsi:
- loop false pada home caraousel kegiatan

No Issues
2025-09-25 11:27:51 +08:00
c2597b25bf upd: push notification
Deskripsi:
- update push notification warning

No Issues
2025-09-25 11:22:39 +08:00
f1b3eecbbe Merge pull request 'amalia/24-sept-25' (#41) from amalia/24-sept-25 into join
Reviewed-on: bip/mobile-darmasaba#41
2025-09-24 17:39:23 +08:00
f042e32d98 upd: push notification ios
Deskripsi:
- push notification foreground dan background pada ios

No Issues
2025-09-24 17:37:00 +08:00
93c492ac71 fix: edit profile
Deskripsi:
- update keyboard avoiding pada edit profile

No Issues
2025-09-24 16:19:33 +08:00
6cca0a3d08 fix : multiline text input
Deskripsi:
- multiline pada text input komentar diskusi umum dan diskusi divisi

No Issues
2025-09-24 16:11:57 +08:00
bbb25a30d2 fix : create divisi
Deskripsi:
- router delete pada saat setelah tambah divisi

No Issues
2025-09-24 14:42:54 +08:00
46e269b45f fix: drawer bottom
Deskripsi:
- scrooll data pada drawer bottom

No Issues
2025-09-24 14:08:04 +08:00
4725d27f74 fix: list project
Deskripsi:
- filter pada setiap user role
- fitur filter disetiap user role

No Issues
2025-09-24 11:01:18 +08:00
ae74791a1c Merge pull request 'amalia/23-sept-25' (#40) from amalia/23-sept-25 into join
Reviewed-on: bip/mobile-darmasaba#40
2025-09-23 17:47:13 +08:00
ba453ad027 upd: ios update 2025-09-23 13:55:50 +08:00
187e9dd19e upd: version code 2025-09-23 12:15:58 +08:00
040cab4f5e Merge pull request 'amalia/22-sept-25' (#39) from amalia/22-sept-25 into join
Reviewed-on: bip/mobile-darmasaba#39
2025-09-22 17:44:20 +08:00
63 changed files with 1077 additions and 414 deletions

View File

@@ -38,4 +38,8 @@ yarn-error.*
app-example
x.ts
x.sh
x.sh
.env
android/

View File

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

View File

@@ -148,7 +148,7 @@ export default function RootLayout() {
}}
/>
</Stack>
<StatusBar style="inverted" translucent={false} backgroundColor="black" />
<StatusBar style={'light'} translucent={false} backgroundColor="black" />
<ToastCustom />
</Provider>
)

View File

@@ -10,6 +10,7 @@ import { router, Stack, useLocalSearchParams } from "expo-router";
import React, { useEffect, useState } from "react";
import { Dimensions, RefreshControl, SafeAreaView, ScrollView, View } from "react-native";
import RenderHTML from 'react-native-render-html';
import Toast from "react-native-toast-message";
import { useSelector } from "react-redux";
type Props = {
@@ -35,10 +36,15 @@ export default function DetailAnnouncement() {
setLoading(loading)
const hasil = await decryptToken(String(token?.current))
const response = await apiGetAnnouncementOne({ id: id, user: hasil })
setData(response.data)
setDataMember(response.member)
if (response.success) {
setData(response.data)
setDataMember(response.member)
} else {
Toast.show({ type: 'small', text1: response.message })
}
} catch (error) {
console.error(error)
Toast.show({ type: 'small', text1: 'Gagal mengambil data' })
} finally {
setLoading(false)
}
@@ -111,6 +117,7 @@ export default function DetailAnnouncement() {
<RenderHTML
contentWidth={contentWidth}
source={{ html: data?.desc }}
baseStyle={{ color: 'black' }}
/>
:
<Text>{data?.desc}</Text>

View File

@@ -154,7 +154,7 @@ export default function EditBanner() {
>
<Entypo name="image" size={50} color={"#aeaeae"} />
<Text style={[Styles.textInformation, Styles.mt05]}>
Mohon unggah gambar dalam resolusi 1535 x 450 piksel untuk
Mohon unggah gambar dalam resolusi 1650 x 720 pixel untuk
memastikan
</Text>
</View>

View File

@@ -137,7 +137,7 @@ export default function CreateBanner() {
>
<Entypo name="image" size={50} color={"#aeaeae"} />
<Text style={[Styles.textInformation, Styles.mt05]}>
Mohon unggah gambar dalam resolusi 1535 x 450 pixel untuk
Mohon unggah gambar dalam resolusi 1650 x 720 pixel untuk
memastikan
</Text>
</View>

View File

@@ -43,7 +43,7 @@ export default function BannerList() {
try {
const hasil = await decryptToken(String(token?.current));
const deletedEntity = await apiDeleteBanner({ user: hasil }, dataId);
if (deletedEntity.success) {
if (deletedEntity.success ) {
Toast.show({ type: 'small', text1: 'Berhasil menghapus data', })
apiGetBanner({ user: hasil }).then((data) =>
dispatch(setEntities(data.data))

View File

@@ -1,24 +1,29 @@
import AlertKonfirmasi from "@/components/alertKonfirmasi";
import BorderBottomItem from "@/components/borderBottomItem";
import ButtonBackHeader from "@/components/buttonBackHeader";
import HeaderRightDiscussionGeneralDetail from "@/components/discussion_general/headerDiscussionDetail";
import DrawerBottom from "@/components/drawerBottom";
import ImageUser from "@/components/imageNew";
import { InputForm } from "@/components/inputForm";
import LabelStatus from "@/components/labelStatus";
import MenuItemRow from "@/components/menuItemRow";
import Skeleton from "@/components/skeleton";
import SkeletonContent from "@/components/skeletonContent";
import Text from '@/components/Text';
import { ColorsStatus } from "@/constants/ColorsStatus";
import { ConstEnv } from "@/constants/ConstEnv";
import { regexOnlySpacesOrEnter } from "@/constants/OnlySpaceOrEnter";
import Styles from "@/constants/Styles";
import { apiGetDiscussionGeneralOne, apiSendDiscussionGeneralCommentar } from "@/lib/api";
import { apiDeleteDiscussionGeneralCommentar, apiGetDiscussionGeneralOne, apiSendDiscussionGeneralCommentar, apiUpdateDiscussionGeneralCommentar } from "@/lib/api";
import { getDB } from "@/lib/firebaseDatabase";
import { useAuthSession } from "@/providers/AuthProvider";
import { Ionicons, MaterialIcons } from "@expo/vector-icons";
import { Feather, Ionicons, MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons";
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 Toast from "react-native-toast-message";
import { useSelector } from "react-redux";
type Props = {
@@ -37,11 +42,14 @@ type PropsKomentar = {
idUser: string
img: string
username: string
isEdited: boolean
updatedAt: string
}
export default function DetailDiscussionGeneral() {
const { token, decryptToken } = useAuthSession()
const entityUser = useSelector((state: any) => state.user)
const entities = useSelector((state: any) => state.entities)
const { id } = useLocalSearchParams<{ id: string }>();
const [data, setData] = useState<Props>()
const [dataKomentar, setDataKomentar] = useState<PropsKomentar[]>([])
@@ -53,6 +61,16 @@ export default function DetailDiscussionGeneral() {
const arrSkeleton = Array.from({ length: 3 }, (_, index) => index)
const reference = ref(getDB(), `/discussion-general/${id}`);
const headerHeight = useHeaderHeight();
const [detailMore, setDetailMore] = useState<any>([])
const [loadingSendKomentar, setLoadingSendKomentar] = useState(false)
const [isVisible, setVisible] = useState(false)
const [selectKomentar, setSelectKomentar] = useState({
id: '',
comment: ''
})
const [viewEdit, setViewEdit] = useState(false)
useEffect(() => {
const onValueChange = reference.on('value', snapshot => {
@@ -115,19 +133,70 @@ export default function DetailDiscussionGeneral() {
async function handleKomentar() {
try {
setLoadingSendKomentar(true)
if (komentar != '') {
const hasil = await decryptToken(String(token?.current))
const response = await apiSendDiscussionGeneralCommentar({ id: id, data: { desc: komentar, user: hasil } })
if (response.success) {
setKomentar('')
updateTrigger()
} else {
Toast.show({ type: 'small', text1: response.message })
}
}
} catch (error) {
console.error(error)
} finally {
setLoadingSendKomentar(false)
}
}
async function handleEditKomentar() {
try {
setLoadingSendKomentar(true)
const hasil = await decryptToken(String(token?.current))
const response = await apiUpdateDiscussionGeneralCommentar({ id: selectKomentar.id, data: { desc: selectKomentar.comment, user: hasil } })
if (response.success) {
updateTrigger()
} else {
Toast.show({ type: 'small', text1: response.message })
}
} catch (error) {
console.error(error)
} finally {
setLoadingSendKomentar(false)
handleViewEditKomentar()
}
}
async function handleDeleteKomentar() {
try {
setLoadingSendKomentar(true)
const hasil = await decryptToken(String(token?.current))
const response = await apiDeleteDiscussionGeneralCommentar({ id: selectKomentar.id, data: { user: hasil } })
if (response.success) {
updateTrigger()
} else {
Toast.show({ type: 'small', text1: response.message })
}
} catch (error) {
console.error(error)
} finally {
setLoadingSendKomentar(false)
setVisible(false)
}
}
function handleMenuKomentar(id: string, comment: string) {
setSelectKomentar({ id, comment })
setVisible(true)
}
function handleViewEditKomentar() {
setVisible(false)
setViewEdit(!viewEdit)
}
return (
<>
<Stack.Screen
@@ -154,13 +223,13 @@ export default function DetailDiscussionGeneral() {
</View>
}
title={data?.title}
titleShowAll={true}
subtitle={
!data?.isActive ?
<LabelStatus category='warning' text='ARSIP' size="small" />
:
<LabelStatus category={data.status == 1 ? 'success' : 'error'} text={data.status == 1 ? 'BUKA' : 'TUTUP'} size="small" />
}
rightTopInfo={data?.createdAt}
desc={data?.desc}
leftBottomInfo={
<View style={[Styles.rowItemsCenter]}>
@@ -168,6 +237,11 @@ export default function DetailDiscussionGeneral() {
<Text style={[Styles.textInformation, Styles.cGray, Styles.mb05]}>{dataKomentar.length} Komentar</Text>
</View>
}
rightBottomInfo={
<View style={[Styles.rowItemsCenter]}>
<Text style={[Styles.textInformation, Styles.cGray, Styles.mb05]}>{data?.createdAt}</Text>
</View>
}
/>
}
<View style={[Styles.p15]}>
@@ -184,12 +258,27 @@ export default function DetailDiscussionGeneral() {
<BorderBottomItem
key={i}
borderType="bottom"
colorPress
icon={
<ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} size="xs" />
}
title={item.username}
rightTopInfo={item.createdAt}
desc={item.comment}
rightBottomInfo={item.isEdited ? "Edited" : ""}
descEllipsize={detailMore.includes(item.id) ? false : true}
onPress={() => {
setDetailMore((prev: any) => {
if (prev.includes(item.id)) {
return prev.filter((id: string) => id !== item.id)
} else {
return [...prev, item.id]
}
})
}}
onLongPress={() => {
item.idUser == entities.id && data?.status != 2 && data?.isActive && handleMenuKomentar(item.id, item.comment)
}}
/>
)
})
@@ -205,27 +294,106 @@ export default function DetailDiscussionGeneral() {
Styles.contentItemCenter,
Styles.w100,
{ backgroundColor: "#f4f4f4" },
viewEdit && Styles.borderTop
]}>
<InputForm
disable={(data?.status === 2 || !data?.isActive || (!memberDiscussion && (entityUser.role == "user" || entityUser.role == "coadmin")))}
type="default"
round
placeholder="Kirim Komentar"
bg="white"
onChange={setKomentar}
value={komentar}
itemRight={
<Pressable onPress={() => {
(komentar != '' && data?.status === 1 && data?.isActive && (memberDiscussion || (entityUser.role != "user" && entityUser.role != "coadmin")))
&& handleKomentar()
}}>
<MaterialIcons name="send" size={25} style={(komentar == '' || data?.status === 2 || !data?.isActive || (!memberDiscussion && (entityUser.role == "user" || entityUser.role == "coadmin"))) ? Styles.cGray : Styles.cDefault} />
</Pressable>
}
/>
{
viewEdit ?
<>
<View style={[Styles.w90, Styles.rowSpaceBetween, Styles.pv05]}>
<View style={[Styles.rowItemsCenter]}>
<Feather name="edit-3" color="black" size={22} style={[Styles.mh05]} />
<Text style={[Styles.textMediumSemiBold]}>Edit Komentar</Text>
</View>
<Pressable onPress={() => handleViewEditKomentar()}>
<MaterialIcons name="close" color="black" size={22} />
</Pressable>
</View>
<InputForm
disable={(data?.status === 2 || !data?.isActive || (!memberDiscussion && (entityUser.role == "user" || entityUser.role == "coadmin")))}
type="default"
round
placeholder="Kirim Komentar"
bg="white"
onChange={(val: string) => setSelectKomentar({ ...selectKomentar, comment: val })}
value={selectKomentar.comment}
multiline
focus={viewEdit}
itemRight={
<Pressable onPress={() => {
(!loadingSendKomentar && selectKomentar.comment != '' && !regexOnlySpacesOrEnter.test(selectKomentar.comment) && data?.status === 1 && data?.isActive && (memberDiscussion || (entityUser.role != "user" && entityUser.role != "coadmin")))
&& handleEditKomentar()
}}
style={[
Platform.OS == 'android' && Styles.mb12,
]}
>
<MaterialIcons name="send" size={25} style={(loadingSendKomentar || selectKomentar.comment == '' || regexOnlySpacesOrEnter.test(selectKomentar.comment) || data?.status === 2 || !data?.isActive || (!memberDiscussion && (entityUser.role == "user" || entityUser.role == "coadmin"))) ? Styles.cGray : Styles.cDefault} />
</Pressable>
}
/>
</>
:
data?.status != 2 && data?.isActive && ((entityUser.role != "user" && entityUser.role != "coadmin") || memberDiscussion)
?
<InputForm
disable={(data?.status === 2 || !data?.isActive || (!memberDiscussion && (entityUser.role == "user" || entityUser.role == "coadmin")))}
type="default"
round
placeholder="Kirim Komentar"
bg="white"
onChange={setKomentar}
value={komentar}
multiline
focus={viewEdit}
itemRight={
<Pressable onPress={() => {
(!loadingSendKomentar && komentar != '' && !regexOnlySpacesOrEnter.test(komentar) && data?.status === 1 && data?.isActive && (memberDiscussion || (entityUser.role != "user" && entityUser.role != "coadmin")))
&& handleKomentar()
}}
style={[
Platform.OS == 'android' && Styles.mb12,
]}
>
<MaterialIcons name="send" size={25} style={(loadingSendKomentar || komentar == '' || regexOnlySpacesOrEnter.test(komentar) || data?.status === 2 || !data?.isActive || (!memberDiscussion && (entityUser.role == "user" || entityUser.role == "coadmin"))) ? Styles.cGray : Styles.cDefault} />
</Pressable>
}
/>
:
<View style={[Styles.pv20, { alignItems: 'center' }]}>
<Text style={[Styles.textInformation, Styles.cGray]}>
{
data?.status == 2 ? "Diskusi telah ditutup" : data?.isActive == false ? "Diskusi telah diarsipkan" : "Hanya anggota diskusi yang dapat memberikan komentar"
}
</Text>
</View>
}
</View>
</KeyboardAvoidingView>
</View >
<DrawerBottom animation="slide" isVisible={isVisible} setVisible={setVisible} title="Komentar">
<View style={Styles.rowItemsCenter}>
<MenuItemRow
icon={<MaterialCommunityIcons name="pencil-outline" color="black" size={25} />}
title="Edit"
onPress={() => { handleViewEditKomentar() }}
/>
<MenuItemRow
icon={<MaterialIcons name="delete" color="black" size={25} />}
title="Hapus"
onPress={() => {
AlertKonfirmasi({
title: 'Konfirmasi',
desc: 'Apakah anda yakin ingin menghapus komentar?',
onPress: () => {
handleDeleteKomentar()
}
})
}}
/>
</View>
</DrawerBottom>
</>
)
}

View File

@@ -98,22 +98,26 @@ export default function Discussion() {
return (
<View style={[Styles.p15, { flex: 1 }]}>
<View>
<View style={[Styles.wrapBtnTab]}>
<ButtonTab
active={status == "false" ? "false" : "true"}
value="true"
onPress={() => { setStatus("true") }}
label="Aktif"
icon={<Feather name="check-circle" color={status == "false" ? 'black' : 'white'} size={20} />}
n={2} />
<ButtonTab
active={status == "false" ? "false" : "true"}
value="false"
onPress={() => { setStatus("false") }}
label="Arsip"
icon={<AntDesign name="closecircleo" color={status == "true" ? 'black' : 'white'} size={20} />}
n={2} />
</View>
{
entityUser.role != "user" && entityUser.role != "coadmin" &&
<View style={[Styles.wrapBtnTab]}>
<ButtonTab
active={status == "false" ? "false" : "true"}
value="true"
onPress={() => { setStatus("true") }}
label="Aktif"
icon={<Feather name="check-circle" color={status == "false" ? 'black' : 'white'} size={20} />}
n={2} />
<ButtonTab
active={status == "false" ? "false" : "true"}
value="false"
onPress={() => { setStatus("false") }}
label="Arsip"
icon={<AntDesign name="closecircleo" color={status == "true" ? 'black' : 'white'} size={20} />}
n={2} />
</View>
}
<InputSearch onChange={setSearch} />
{
(entityUser.role == "supadmin" || entityUser.role == "developer") &&

View File

@@ -135,22 +135,25 @@ export default function MemberDiscussionDetail() {
router.push(`/member/${chooseUser.idUser}`)
}}
/>
{
entityUser.role != "user" && entityUser.role != "coadmin" &&
<MenuItemRow
icon={<MaterialCommunityIcons name="account-remove" color="black" size={25} />}
title="Keluarkan"
onPress={() => {
setModal(false)
AlertKonfirmasi({
title: 'Konfirmasi',
desc: 'Apakah Anda yakin ingin mengeluarkan anggota?',
onPress: () => {
handleDeleteUser()
}
})
<MenuItemRow
icon={<MaterialCommunityIcons name="account-remove" color="black" size={25} />}
title="Keluarkan"
onPress={() => {
setModal(false)
AlertKonfirmasi({
title: 'Konfirmasi',
desc: 'Apakah Anda yakin ingin mengeluarkan anggota?',
onPress: () => {
handleDeleteUser()
}
})
}}
/>
}
}}
/>
</View>
</DrawerBottom>
</SafeAreaView>

View File

@@ -1,27 +1,34 @@
import AlertKonfirmasi from "@/components/alertKonfirmasi";
import BorderBottomItem from "@/components/borderBottomItem";
import ButtonBackHeader from "@/components/buttonBackHeader";
import HeaderRightDiscussionDetail from "@/components/discussion/headerDiscussionDetail";
import DrawerBottom from "@/components/drawerBottom";
import ImageUser from "@/components/imageNew";
import { InputForm } from "@/components/inputForm";
import LabelStatus from "@/components/labelStatus";
import MenuItemRow from "@/components/menuItemRow";
import Skeleton from "@/components/skeleton";
import SkeletonContent from "@/components/skeletonContent";
import Text from "@/components/Text";
import { ConstEnv } from "@/constants/ConstEnv";
import { regexOnlySpacesOrEnter } from "@/constants/OnlySpaceOrEnter";
import Styles from "@/constants/Styles";
import {
apiDeleteDiscussionCommentar,
apiEditDiscussionCommentar,
apiGetDiscussionOne,
apiGetDivisionOneFeature,
apiSendDiscussionCommentar,
} from "@/lib/api";
import { getDB } from "@/lib/firebaseDatabase";
import { useAuthSession } from "@/providers/AuthProvider";
import { Ionicons, MaterialIcons } from "@expo/vector-icons";
import { Feather, Ionicons, MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons";
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, RefreshControl, ScrollView, View } from "react-native";
import Toast from "react-native-toast-message";
import { useSelector } from "react-redux";
type Props = {
@@ -43,6 +50,9 @@ type PropsComment = {
createdAt: string;
username: string;
img: string;
idUser: string;
isEdited: boolean;
updatedAt: string;
};
export default function DiscussionDetail() {
@@ -63,6 +73,15 @@ export default function DiscussionDetail() {
const reference = ref(getDB(), `/discussion-division/${detail}`);
const [refreshing, setRefreshing] = useState(false)
const headerHeight = useHeaderHeight();
const [detailMore, setDetailMore] = useState<any>([])
const entities = useSelector((state: any) => state.entities)
const [isVisible, setVisible] = useState(false)
const [selectKomentar, setSelectKomentar] = useState({
id: '',
comment: ''
})
const [viewEdit, setViewEdit] = useState(false)
useEffect(() => {
@@ -170,6 +189,59 @@ export default function DiscussionDetail() {
}
}
async function handleEditKomentar() {
try {
setLoadingSend(true);
const hasil = await decryptToken(String(token?.current));
const response = await apiEditDiscussionCommentar({
id: selectKomentar.id,
data: { comment: selectKomentar.comment, user: hasil },
});
if (response.success) {
updateTrigger()
} else {
Toast.show({ type: 'small', text1: response.message })
}
} catch (error) {
console.error(error);
} finally {
setLoadingSend(false);
handleViewEditKomentar()
}
}
async function handleDeleteKomentar() {
try {
setLoadingSend(true);
const hasil = await decryptToken(String(token?.current));
const response = await apiDeleteDiscussionCommentar({
id: selectKomentar.id,
data: { user: hasil },
});
if (response.success) {
updateTrigger()
} else {
Toast.show({ type: 'small', text1: response.message })
}
} catch (error) {
console.error(error);
} finally {
setLoadingSend(false)
setVisible(false)
}
}
function handleMenuKomentar(id: string, comment: string) {
setSelectKomentar({ id, comment })
setVisible(true)
}
function handleViewEditKomentar() {
setVisible(false)
setViewEdit(!viewEdit)
}
const handleRefresh = async () => {
setRefreshing(true)
@@ -266,6 +338,7 @@ export default function DiscussionDetail() {
<BorderBottomItem
key={index}
borderType="bottom"
colorPress
icon={
<ImageUser
src={`${ConstEnv.url_storage}/files/${item.img}`}
@@ -275,7 +348,20 @@ export default function DiscussionDetail() {
title={item.username}
rightTopInfo={item.createdAt}
desc={item.comment}
descEllipsize={false}
rightBottomInfo={item.isEdited ? "Edited" : ""}
descEllipsize={detailMore.includes(item.id) ? false : true}
onPress={() => {
setDetailMore((prev: any) => {
if (prev.includes(item.id)) {
return prev.filter((id: string) => id !== item.id)
} else {
return [...prev, item.id]
}
})
}}
onLongPress={() => {
item.idUser == entities.id && data?.status != 2 && data?.isActive && handleMenuKomentar(item.id, item.comment)
}}
/>
))
}
@@ -292,60 +378,145 @@ export default function DiscussionDetail() {
Styles.contentItemCenter,
Styles.w100,
{ backgroundColor: "#f4f4f4" },
viewEdit && Styles.borderTop
]}
>
<InputForm
disable={
data?.status == 2 ||
data?.isActive == false ||
((entityUser.role == "user" || entityUser.role == "coadmin") &&
!isMemberDivision)
}
bg="white"
type="default"
round
placeholder="Kirim Komentar"
onChange={setKomentar}
value={komentar}
itemRight={
<Pressable
onPress={() => {
komentar != "" &&
!loadingSend &&
data?.status != 2 &&
data?.isActive &&
(((entityUser.role == "user" ||
entityUser.role == "coadmin") &&
isMemberDivision) ||
entityUser.role == "admin" ||
entityUser.role == "supadmin" ||
entityUser.role == "developer" ||
entityUser.role == "cosupadmin") &&
handleKomentar();
}}
>
<MaterialIcons
name="send"
size={25}
style={
komentar == "" ||
loadingSend ||
data?.status == 2 ||
data?.isActive == false ||
((entityUser.role == "user" ||
entityUser.role == "coadmin") &&
!isMemberDivision)
? Styles.cGray
: Styles.cDefault
{
viewEdit ?
<>
<View style={[Styles.w90, Styles.rowSpaceBetween, Styles.pv05]}>
<View style={[Styles.rowItemsCenter]}>
<Feather name="edit-3" color="black" size={22} style={[Styles.mh05]} />
<Text style={[Styles.textMediumSemiBold]}>Edit Komentar</Text>
</View>
<Pressable onPress={() => handleViewEditKomentar()}>
<MaterialIcons name="close" color="black" size={22} />
</Pressable>
</View>
<InputForm
bg="white"
type="default"
round
multiline
placeholder="Kirim Komentar"
onChange={(val: string) => setSelectKomentar({ ...selectKomentar, comment: val })}
value={selectKomentar.comment}
itemRight={
<Pressable
onPress={() => {
selectKomentar.comment != "" &&
!regexOnlySpacesOrEnter.test(selectKomentar.comment) &&
!loadingSend &&
data?.status != 2 &&
data?.isActive &&
(((entityUser.role == "user" ||
entityUser.role == "coadmin") &&
isMemberDivision) ||
entityUser.role == "admin" ||
entityUser.role == "supadmin" ||
entityUser.role == "developer" ||
entityUser.role == "cosupadmin") &&
handleEditKomentar();
}}
style={[
Platform.OS == 'android' && Styles.mb12,
]}
>
<MaterialIcons
name="send"
size={25}
style={
[selectKomentar.comment == "" || regexOnlySpacesOrEnter.test(selectKomentar.comment) || loadingSend || ((entityUser.role == "user" || entityUser.role == "coadmin") && !isMemberDivision)
? Styles.cGray
: Styles.cDefault,
]
}
/>
</Pressable>
}
/>
</Pressable>
}
/>
</>
:
data?.status != 2 && data?.isActive && ((entityUser.role != "user" && entityUser.role != "coadmin") ||
isMemberDivision)
?
<InputForm
bg="white"
type="default"
round
multiline
placeholder="Kirim Komentar"
onChange={setKomentar}
value={komentar}
itemRight={
<Pressable
onPress={() => {
komentar != "" &&
!regexOnlySpacesOrEnter.test(komentar) &&
!loadingSend &&
data?.status != 2 &&
data?.isActive &&
(((entityUser.role == "user" ||
entityUser.role == "coadmin") &&
isMemberDivision) ||
entityUser.role == "admin" ||
entityUser.role == "supadmin" ||
entityUser.role == "developer" ||
entityUser.role == "cosupadmin") &&
handleKomentar();
}}
style={[
Platform.OS == 'android' && Styles.mb12,
]}
>
<MaterialIcons
name="send"
size={25}
style={
[komentar == "" || regexOnlySpacesOrEnter.test(komentar) || loadingSend || ((entityUser.role == "user" || entityUser.role == "coadmin") && !isMemberDivision)
? Styles.cGray
: Styles.cDefault,
]
}
/>
</Pressable>
}
/>
:
<View style={[Styles.pv20, { alignItems: 'center' }]}>
<Text style={[Styles.textInformation, Styles.cGray]}>
{
data?.status == 2 ? "Diskusi telah ditutup" : data?.isActive == false ? "Diskusi telah diarsipkan" : "Hanya anggota divisi yang dapat memberikan komentar"
}
</Text>
</View>
}
</View>
</KeyboardAvoidingView>
</View>
<DrawerBottom animation="slide" isVisible={isVisible} setVisible={setVisible} title="Komentar">
<View style={Styles.rowItemsCenter}>
<MenuItemRow
icon={<MaterialCommunityIcons name="pencil-outline" color="black" size={25} />}
title="Edit"
onPress={() => { handleViewEditKomentar() }}
/>
<MenuItemRow
icon={<MaterialIcons name="delete" color="black" size={25} />}
title="Hapus"
onPress={() => {
AlertKonfirmasi({
title: 'Konfirmasi',
desc: 'Apakah anda yakin ingin menghapus komentar?',
onPress: () => {
handleDeleteKomentar()
}
})
}}
/>
</View>
</DrawerBottom>
</>
);
}

View File

@@ -7,7 +7,7 @@ import SkeletonContent from "@/components/skeletonContent";
import Text from "@/components/Text";
import { ConstEnv } from "@/constants/ConstEnv";
import Styles from "@/constants/Styles";
import { apiGetDiscussion } from "@/lib/api";
import { apiGetDiscussion, apiGetDivisionOneFeature } from "@/lib/api";
import { useAuthSession } from "@/providers/AuthProvider";
import { AntDesign, Feather, Ionicons } from "@expo/vector-icons";
import { router, useLocalSearchParams } from "expo-router";
@@ -41,6 +41,30 @@ export default function DiscussionDivision() {
const [waiting, setWaiting] = useState(false)
const [status, setStatus] = useState<'true' | 'false'>('true')
const [refreshing, setRefreshing] = useState(false)
const [isMemberDivision, setIsMemberDivision] = useState(false)
const [isAdminDivision, setIsAdminDivision] = useState(false)
const entityUser = useSelector((state: any) => state.user)
async function handleCheckMember() {
try {
const hasil = await decryptToken(String(token?.current));
const response = await apiGetDivisionOneFeature({
id,
user: hasil,
cat: "check-member",
});
const response2 = await apiGetDivisionOneFeature({
id,
user: hasil,
cat: "check-admin",
});
setIsMemberDivision(response.data);
setIsAdminDivision(response2.data);
} catch (error) {
console.error(error);
}
}
async function handleLoad(loading: boolean, thisPage: number) {
try {
@@ -80,6 +104,10 @@ export default function DiscussionDivision() {
}, 1000);
}
useEffect(() => {
handleCheckMember()
}, [])
const handleRefresh = async () => {
setRefreshing(true)
handleLoad(false, 1)
@@ -101,25 +129,29 @@ export default function DiscussionDivision() {
return (
<View style={[Styles.p15, { flex: 1 }]}>
<View>
<View style={[Styles.wrapBtnTab]}>
<ButtonTab
active={status == "false" ? "false" : "true"}
value="true"
onPress={() => { setStatus("true") }}
label="Aktif"
icon={<Feather name="check-circle" color={status == "false" ? 'black' : 'white'} size={20} />}
n={2} />
<ButtonTab
active={status == "false" ? "false" : "true"}
value="false"
onPress={() => { setStatus("false") }}
label="Arsip"
icon={<AntDesign name="closecircleo" color={status == "true" ? 'black' : 'white'} size={20} />}
n={2} />
{
((entityUser.role != "user" && entityUser.role != "coadmin") || isAdminDivision) &&
<View>
<View style={[Styles.wrapBtnTab]}>
<ButtonTab
active={status == "false" ? "false" : "true"}
value="true"
onPress={() => { setStatus("true") }}
label="Aktif"
icon={<Feather name="check-circle" color={status == "false" ? 'black' : 'white'} size={20} />}
n={2} />
<ButtonTab
active={status == "false" ? "false" : "true"}
value="false"
onPress={() => { setStatus("false") }}
label="Arsip"
icon={<AntDesign name="closecircleo" color={status == "true" ? 'black' : 'white'} size={20} />}
n={2} />
</View>
<InputSearch onChange={setSearch} />
</View>
<InputSearch onChange={setSearch} />
</View>
}
<View style={[{ flex: 2 }, Styles.mt05]}>
{

View File

@@ -18,6 +18,7 @@ import Styles from "@/constants/Styles";
import {
apiDocumentDelete,
apiDocumentRename,
apiGetDivisionOneFeature,
apiGetDocument,
apiShareDocument,
} from "@/lib/api";
@@ -85,6 +86,8 @@ export default function DocumentDivision() {
const update = useSelector((state: any) => state.dokumenUpdate)
const [refreshing, setRefreshing] = useState(false)
const [loadingOpen, setLoadingOpen] = useState(false)
const [isMemberDivision, setIsMemberDivision] = useState(false)
const entityUser = useSelector((state: any) => state.user)
const [bodyRename, setBodyRename] = useState({
id: "",
name: "",
@@ -93,6 +96,24 @@ export default function DocumentDivision() {
extension: "",
});
async function handleCheckMember() {
try {
const hasil = await decryptToken(String(token?.current));
const response = await apiGetDivisionOneFeature({
id,
user: hasil,
cat: "check-member",
});
setIsMemberDivision(response.data);
} catch (error) {
console.error(error);
}
}
useEffect(() => {
handleCheckMember()
}, [id])
async function handleLoad(loading: boolean) {
try {
setLoading(loading)
@@ -347,7 +368,7 @@ export default function DocumentDivision() {
}}
/>
) : (
<HeaderRightDocument path={path} />
<HeaderRightDocument path={path} isMember={isMemberDivision} />
),
}}
/>
@@ -407,6 +428,7 @@ export default function DocumentDivision() {
: `${item.name}.${item.extension}`
}
dateTime={item.createdAt}
canChecked={(entityUser.role != "user" && entityUser.role != "coadmin") || isMemberDivision}
onChecked={() => {
handleCheckboxChange(index);
}}

View File

@@ -12,7 +12,7 @@ import { useAuthSession } from "@/providers/AuthProvider";
import { AntDesign } from "@expo/vector-icons";
import { router, Stack, useLocalSearchParams } from "expo-router";
import { useEffect, useState } from "react";
import { Pressable, SafeAreaView, ScrollView, View } from "react-native";
import { Pressable, ScrollView, View } from "react-native";
import Toast from "react-native-toast-message";
import { useDispatch, useSelector } from "react-redux";
@@ -94,7 +94,7 @@ export default function AddMemberTask() {
return (
<SafeAreaView>
<>
<Stack.Screen
options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
@@ -111,7 +111,7 @@ export default function AddMemberTask() {
)
}}
/>
<View style={[Styles.p15, Styles.mb100]}>
<View style={[Styles.p15, { flex: 1 }]}>
<InputSearch onChange={(val) => setSearch(val)} value={search} />
{
@@ -172,6 +172,6 @@ export default function AddMemberTask() {
}
</ScrollView>
</View>
</SafeAreaView>
</>
)
}

View File

@@ -125,7 +125,7 @@ export default function DetailTaskDivision() {
<SectionTanggalTugasTask refreshing={refreshing} isMemberDivision={isMemberDivision} />
<SectionFileTask refreshing={refreshing} isMemberDivision={isMemberDivision} />
<SectionLinkTask refreshing={refreshing} isMemberDivision={isMemberDivision} />
<SectionMemberTask refreshing={refreshing} isMemberDivision={isMemberDivision} />
<SectionMemberTask refreshing={refreshing} isAdminDivision={isAdminDivision} />
</View>
</ScrollView>
</SafeAreaView>

View File

@@ -12,7 +12,7 @@ import { useAuthSession } from "@/providers/AuthProvider";
import { AntDesign } from "@expo/vector-icons";
import { router, Stack, useLocalSearchParams } from "expo-router";
import { useEffect, useState } from "react";
import { Pressable, SafeAreaView, ScrollView, View } from "react-native";
import { Pressable, ScrollView, View } from "react-native";
import Toast from "react-native-toast-message";
import { useDispatch, useSelector } from "react-redux";
@@ -64,7 +64,7 @@ export default function AddMemberCreateTask() {
return (
<SafeAreaView>
<>
<Stack.Screen
options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
@@ -81,7 +81,7 @@ export default function AddMemberCreateTask() {
)
}}
/>
<View style={[Styles.p15]}>
<View style={[Styles.p15, { flex: 1 }]}>
<InputSearch onChange={(val) => setSearch(val)} value={search} />
{
@@ -138,6 +138,6 @@ export default function AddMemberCreateTask() {
}
</ScrollView>
</View>
</SafeAreaView>
</>
)
}

View File

@@ -99,14 +99,18 @@ export default function CreateTaskAddTugas() {
timeStart: item.timeStart,
timeEnd: item.timeEnd,
}))
dispatch(setTaskCreate([...taskCreate, {
const hasilOrder = [...taskCreate, {
title: title,
dateStart: from,
dateEnd: to,
dateStartFix: formatDateOnly(range.startDate, "YYYY-MM-DD"),
dateEndFix: formatDateOnly(range.endDate, "YYYY-MM-DD"),
dataDetail: dataDetailFix
}]))
}].sort((a, b) => {
return new Date(a.dateStartFix).getTime() - new Date(b.dateStartFix).getTime();
});
dispatch(setTaskCreate(hasilOrder))
router.back();
} catch (error) {
console.error(error);

View File

@@ -45,6 +45,8 @@ export default function AddMemberDivision() {
setData(responsemember.data.filter((i: any) => i.idUserRole != 'supadmin'))
} catch (error) {
console.error(error)
} finally {
setLoading(false)
}
}
@@ -95,7 +97,7 @@ export default function AddMemberDivision() {
return (
<SafeAreaView>
<>
<Stack.Screen
options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
@@ -112,7 +114,7 @@ export default function AddMemberDivision() {
)
}}
/>
<View style={[Styles.p15]}>
<View style={[Styles.p15, { flex: 1 }]}>
<InputSearch onChange={(val) => handleSearch(val)} value={search} />
{
@@ -173,6 +175,6 @@ export default function AddMemberDivision() {
}
</ScrollView>
</View>
</SafeAreaView>
</>
)
}

View File

@@ -11,12 +11,12 @@ import Text from "@/components/Text"
import { ColorsStatus } from "@/constants/ColorsStatus"
import { ConstEnv } from "@/constants/ConstEnv"
import Styles from "@/constants/Styles"
import { apiDeleteMemberDivision, apiGetDivisionOneDetail, apiUpdateStatusAdminDivision } from "@/lib/api"
import { apiDeleteMemberDivision, apiGetDivisionOneDetail, apiGetDivisionOneFeature, apiUpdateStatusAdminDivision } from "@/lib/api"
import { useAuthSession } from "@/providers/AuthProvider"
import { Feather, MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons"
import { router, Stack, useLocalSearchParams } from "expo-router"
import { useEffect, useState } from "react"
import { Pressable, SafeAreaView, ScrollView, View } from "react-native"
import { Pressable, RefreshControl, SafeAreaView, ScrollView, View } from "react-native"
import Toast from "react-native-toast-message"
import { useSelector } from "react-redux"
@@ -39,6 +39,8 @@ type PropsMember = {
}
export default function InformationDivision() {
const [refreshing, setRefreshing] = useState(false)
const entityUser = useSelector((state: any) => state.user)
const { id } = useLocalSearchParams<{ id: string }>()
const [isModal, setModal] = useState(false)
const { token, decryptToken } = useAuthSession()
@@ -48,6 +50,8 @@ export default function InformationDivision() {
const update = useSelector((state: any) => state.divisionUpdate)
const arrSkeleton = Array.from({ length: 5 }, (_, index) => index)
const [loading, setLoading] = useState(true)
const [isMemberDivision, setIsMemberDivision] = useState(false)
const [isAdminDivision, setIsAdminDivision] = useState(false)
const [dataMemberChoose, setDataMemberChoose] = useState({
id: '',
name: '',
@@ -113,12 +117,42 @@ export default function InformationDivision() {
}
}
const handleRefresh = async () => {
setRefreshing(true)
handleLoad(false)
handleCheckMember()
await new Promise(resolve => setTimeout(resolve, 2000));
setRefreshing(false)
};
async function handleCheckMember() {
try {
const hasil = await decryptToken(String(token?.current));
const response = await apiGetDivisionOneFeature({
id,
user: hasil,
cat: "check-member",
});
const response2 = await apiGetDivisionOneFeature({
id,
user: hasil,
cat: "check-admin",
});
setIsMemberDivision(response.data);
setIsAdminDivision(response2.data);
} catch (error) {
console.error(error);
}
}
useEffect(() => {
handleLoad(false)
}, [refresh, update])
useEffect(() => {
handleLoad(true)
handleCheckMember()
}, [])
function handleChooseMember(item: PropsMember) {
@@ -133,10 +167,18 @@ export default function InformationDivision() {
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: 'Informasi Divisi',
headerTitleAlign: 'center',
headerRight: () => <HeaderRightDivisionInfo id={id} active={dataDetail?.isActive} />,
headerRight: () => ((entityUser.role != "user" && entityUser.role != "coadmin") || isAdminDivision) && <HeaderRightDivisionInfo id={id} active={dataDetail?.isActive} />,
}}
/>
<ScrollView style={[Styles.h100]}>
<ScrollView
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={handleRefresh}
/>
}
style={[Styles.h100]}
>
<View style={[Styles.p15]}>
{
dataDetail?.isActive == false && (
@@ -161,6 +203,7 @@ export default function InformationDivision() {
<Text style={[Styles.textDefault, Styles.mv05]}>{dataMember.length} Anggota</Text>
<View style={[Styles.wrapPaper]}>
{
((entityUser.role != "user" && entityUser.role != "coadmin") || isAdminDivision) &&
dataDetail?.isActive && (
<BorderBottomItem
onPress={() => { router.push(`/division/${id}/add-member`) }}
@@ -188,7 +231,7 @@ export default function InformationDivision() {
<BorderBottomItem
key={index}
borderType="bottom"
onPress={() => { dataDetail?.isActive && handleChooseMember(item) }}
onPress={() => { dataDetail?.isActive && (isAdminDivision || (entityUser.role != "user" && entityUser.role != "coadmin")) && handleChooseMember(item) }}
icon={
<ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} size="sm" />
}

View File

@@ -1,22 +1,28 @@
import AlertKonfirmasi from "@/components/alertKonfirmasi";
import ButtonBackHeader from "@/components/buttonBackHeader";
import ButtonNextHeader from "@/components/buttonNextHeader";
import { InputForm } from "@/components/inputForm";
import ModalSelect from "@/components/modalSelect";
import SelectForm from "@/components/selectForm";
import Styles from "@/constants/Styles";
import { apiCheckDivisionName } from "@/lib/api";
import { setFormCreateDivision } from "@/lib/divisionCreate";
import { useAuthSession } from "@/providers/AuthProvider";
import { router, Stack } from "expo-router";
import { useEffect, 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 CreateDivision() {
const [isSelect, setSelect] = useState(false);
const [chooseGroup, setChooseGroup] = useState({ val: "", label: "" });
const dispatch = useDispatch();
const update = useSelector((state: any) => state.divisionCreate);
const { token, decryptToken } = useAuthSession()
const [isSelect, setSelect] = useState(false)
const [chooseGroup, setChooseGroup] = useState({ val: "", label: "" })
const dispatch = useDispatch()
const update = useSelector((state: any) => state.divisionCreate)
const entityUser = useSelector((state: any) => state.user)
const userLogin = useSelector((state: any) => state.entities)
const [loadingBtn, setLoadingBtn] = useState(false)
const [error, setError] = useState({
idGroup: false,
name: false,
@@ -54,7 +60,34 @@ export default function CreateDivision() {
}
}
function handleSetData() {
async function handleCheckName() {
try {
setLoadingBtn(true)
const hasil = await decryptToken(String(token?.current))
const response = await apiCheckDivisionName({ data: { ...dataForm }, user: hasil })
if (response.success) {
if (!response.available) {
AlertKonfirmasi({
title: 'Peringatan',
category: 'warning',
desc: 'Nama divisi sudah ada. Tidak dapat membuat divisi dengan nama yang sama',
onPress: () => { }
})
} else {
handleSetData()
}
} else {
Toast.show({ type: 'small', text1: response.message, })
}
} catch (error) {
console.error(error)
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
} finally {
setLoadingBtn(false)
}
}
async function handleSetData() {
dispatch(setFormCreateDivision({ ...update, data: dataForm }))
router.push(`./create/add-member`)
}
@@ -80,8 +113,8 @@ export default function CreateDivision() {
headerTitleAlign: "center",
headerRight: () => (
<ButtonNextHeader
onPress={() => { handleSetData() }}
disable={error.idGroup || error.name || chooseGroup.val == "" || chooseGroup.val == "null" || dataForm.name == "" || dataForm.name == "null"}
onPress={() => { handleCheckName() }}
disable={loadingBtn || error.idGroup || error.name || chooseGroup.val == "" || chooseGroup.val == "null" || dataForm.name == "" || dataForm.name == "null"}
/>
),
}}

View File

@@ -9,6 +9,7 @@ import { setFormCreateDivision } from "@/lib/divisionCreate";
import { setUpdateDivision } from "@/lib/divisionUpdate";
import { useAuthSession } from "@/providers/AuthProvider";
import { AntDesign } from "@expo/vector-icons";
import { StackActions, useNavigation } from "@react-navigation/native";
import { router, Stack, useLocalSearchParams } from "expo-router";
import { useEffect, useState } from "react";
import { Pressable, SafeAreaView, ScrollView, View } from "react-native";
@@ -23,6 +24,7 @@ type Props = {
export default function CreateDivisionAddAdmin() {
const { token, decryptToken } = useAuthSession()
const navigation = useNavigation()
const { id } = useLocalSearchParams<{ id: string }>()
const [dataOld, setDataOld] = useState<Props[]>([])
const [data, setData] = useState<Props[]>([])
@@ -57,7 +59,8 @@ export default function CreateDivisionAddAdmin() {
Toast.show({ type: 'small', text1: 'Berhasil membuat divisi', })
dispatch(setFormCreateDivision({ admin: [], member: [], data: { idGroup: '', name: '', desc: '' } }))
dispatch(setUpdateDivision(!updateDivision))
router.replace(`/division/`)
navigation.dispatch(StackActions.pop(3))
// router.replace(`/division/`)
} else {
Toast.show({ type: 'small', text1: response.message, })
}
@@ -71,7 +74,7 @@ export default function CreateDivisionAddAdmin() {
return (
<SafeAreaView>
<>
<Stack.Screen
options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
@@ -88,7 +91,7 @@ export default function CreateDivisionAddAdmin() {
)
}}
/>
<View style={[Styles.p15]}>
<View style={[Styles.p15, { flex: 1 }]}>
<ScrollView>
{
data.length > 0 ?
@@ -123,6 +126,6 @@ export default function CreateDivisionAddAdmin() {
}
</ScrollView>
</View>
</SafeAreaView>
</>
)
}

View File

@@ -12,7 +12,7 @@ import { useAuthSession } from "@/providers/AuthProvider";
import { AntDesign } from "@expo/vector-icons";
import { router, Stack, useLocalSearchParams } from "expo-router";
import { useEffect, useState } from "react";
import { Pressable, SafeAreaView, ScrollView, View } from "react-native";
import { Pressable, ScrollView, View } from "react-native";
import { useDispatch, useSelector } from "react-redux";
type Props = {
@@ -60,7 +60,7 @@ export default function CreateDivisionAddMember() {
return (
<SafeAreaView>
<>
<Stack.Screen
options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
@@ -74,7 +74,7 @@ export default function CreateDivisionAddMember() {
)
}}
/>
<View style={[Styles.p15]}>
<View style={[Styles.p15, { flex: 1 }]}>
<InputSearch onChange={(val) => setSearch(val)} value={search} />
{
@@ -135,6 +135,6 @@ export default function CreateDivisionAddMember() {
}
</ScrollView>
</View>
</SafeAreaView>
</>
)
}

View File

@@ -8,13 +8,17 @@ import { ConstEnv } from "@/constants/ConstEnv";
import Styles from "@/constants/Styles";
import { apiEditProfile, apiGetProfile } from "@/lib/api";
import { setEntities } from "@/lib/entitiesSlice";
import { validateName } from "@/lib/fun_validateName";
import { useAuthSession } from "@/providers/AuthProvider";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import { useHeaderHeight } from "@react-navigation/elements";
import * as ImagePicker from "expo-image-picker";
import { router, Stack } from "expo-router";
import { useEffect, useState } from "react";
import {
Image,
KeyboardAvoidingView,
Platform,
Pressable,
SafeAreaView,
ScrollView,
@@ -37,6 +41,7 @@ type Props = {
};
export default function EditProfile() {
const headerHeight = useHeaderHeight()
const dispatch = useDispatch()
const entities = useSelector((state: any) => state.entities)
const { token, decryptToken } = useAuthSession()
@@ -105,7 +110,7 @@ export default function EditProfile() {
}
} else if (cat == "name") {
setData({ ...data, name: val });
if (val == "") {
if (!validateName(val)) {
setError({ ...error, name: true });
} else {
setError({ ...error, name: false });
@@ -160,8 +165,8 @@ export default function EditProfile() {
if (imgForm != undefined) {
fd.append("file", {
uri: imgForm.uri,
type: imgForm.mimeType,
name: imgForm.fileName,
type: imgForm.mimeType || "image/jpeg",
name: imgForm.fileName || "image.jpg",
} as any);
} else {
fd.append("file", "undefined",);
@@ -193,9 +198,9 @@ export default function EditProfile() {
const pickImageAsync = async () => {
let result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ["images"],
allowsEditing: false,
quality: 1,
aspect: [1, 1],
allowsEditing: true,
quality: 0.9,
aspect: [1, 1]
});
if (!result.canceled) {
@@ -231,116 +236,122 @@ export default function EditProfile() {
),
}}
/>
<ScrollView style={[Styles.h100]}>
<View style={[Styles.p15]}>
<View style={{ justifyContent: "center", alignItems: "center" }}>
{
selectedImage != undefined ? (
<Pressable onPress={pickImageAsync}>
<Image
src={
typeof selectedImage === "string"
? selectedImage
: selectedImage.uri
}
style={[Styles.userProfileBig]}
onError={() => { setErrorImg(true) }}
/>
<View style={[Styles.absoluteIconPicker]}>
<MaterialCommunityIcons name="image" color={'white'} size={15} />
</View>
</Pressable>
) : (
<Pressable onPress={pickImageAsync}>
<Image
source={errorImg ? require("../../assets/images/user.jpg") : { uri: `${ConstEnv.url_storage}/files/${data?.img}` }}
style={[Styles.userProfileBig]}
onError={() => { setErrorImg(true) }}
/>
<View style={[Styles.absoluteIconPicker]}>
<MaterialCommunityIcons name="image" color={'white'} size={15} />
</View>
</Pressable>
)
}
<KeyboardAvoidingView
style={[Styles.h100]}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
keyboardVerticalOffset={headerHeight}
>
<ScrollView showsVerticalScrollIndicator={false}>
<View style={[Styles.p15]}>
<View style={{ justifyContent: "center", alignItems: "center" }}>
{
selectedImage != undefined ? (
<Pressable onPress={pickImageAsync}>
<Image
src={
typeof selectedImage === "string"
? selectedImage
: selectedImage.uri
}
style={[Styles.userProfileBig]}
onError={() => { setErrorImg(true) }}
/>
<View style={[Styles.absoluteIconPicker]}>
<MaterialCommunityIcons name="image" color={'white'} size={15} />
</View>
</Pressable>
) : (
<Pressable onPress={pickImageAsync}>
<Image
source={errorImg ? require("../../assets/images/user.jpg") : { uri: `${ConstEnv.url_storage}/files/${data?.img}` }}
style={[Styles.userProfileBig]}
onError={() => { setErrorImg(true) }}
/>
<View style={[Styles.absoluteIconPicker]}>
<MaterialCommunityIcons name="image" color={'white'} size={15} />
</View>
</Pressable>
)
}
</View>
<SelectForm
label="Jabatan"
placeholder="Pilih Jabatan"
value={choosePosition.label}
required
onPress={() => {
setValChoose(choosePosition.val);
setValSelect("position");
setSelect(true);
}}
error={error.position}
errorText="Jabatan tidak boleh kosong"
/>
<InputForm
label="NIK"
type="numeric"
placeholder="NIK"
required
value={data?.nik}
error={error.nik}
errorText="NIK Harus 16 Karakter"
onChange={val => {
validationForm("nik", val)
}}
/>
<InputForm
label="Nama"
type="default"
placeholder="Nama"
required
value={data?.name}
error={error.name}
errorText="Nama harus 350 karakter (huruf, angka, spasi, dan simbol ringan (. , ' _ -))"
onChange={val => {
validationForm("name", val)
}}
/>
<InputForm
label="Email"
type="default"
placeholder="Email"
required
value={data?.email}
error={error.email}
errorText="Email tidak valid"
onChange={val => {
validationForm("email", val)
}}
/>
<InputForm
label="Nomor Telepon"
type="numeric"
placeholder="8XX-XXX-XXX"
required
itemLeft={<Text>+62</Text>}
value={data?.phone}
error={error.phone}
errorText="Nomor Telepon tidak valid"
onChange={val => {
validationForm("phone", val)
}}
/>
<SelectForm
label="Jenis Kelamin"
placeholder="Pilih Jenis Kelamin"
value={chooseGender.label}
required
onPress={() => {
setValChoose(chooseGender.val);
setValSelect("gender");
setSelect(true);
}}
error={error.gender}
errorText="Jenis Kelamin tidak boleh kosong"
/>
</View>
<SelectForm
label="Jabatan"
placeholder="Pilih Jabatan"
value={choosePosition.label}
required
onPress={() => {
setValChoose(choosePosition.val);
setValSelect("position");
setSelect(true);
}}
error={error.position}
errorText="Jabatan tidak boleh kosong"
/>
<InputForm
label="NIK"
type="numeric"
placeholder="NIK"
required
value={data?.nik}
error={error.nik}
errorText="NIK Harus 16 Karakter"
onChange={val => {
validationForm("nik", val)
}}
/>
<InputForm
label="Nama"
type="default"
placeholder="Nama"
required
value={data?.name}
error={error.name}
errorText="Nama tidak boleh kosong"
onChange={val => {
validationForm("name", val)
}}
/>
<InputForm
label="Email"
type="default"
placeholder="Email"
required
value={data?.email}
error={error.email}
errorText="Email tidak valid"
onChange={val => {
validationForm("email", val)
}}
/>
<InputForm
label="Nomor Telepon"
type="numeric"
placeholder="8XX-XXX-XXX"
required
itemLeft={<Text>+62</Text>}
value={data?.phone}
error={error.phone}
errorText="Nomor Telepon tidak valid"
onChange={val => {
validationForm("phone", val)
}}
/>
<SelectForm
label="Jenis Kelamin"
placeholder="Pilih Jenis Kelamin"
value={chooseGender.label}
required
onPress={() => {
setValChoose(chooseGender.val);
setValSelect("gender");
setSelect(true);
}}
error={error.gender}
errorText="Jenis Kelamin tidak boleh kosong"
/>
</View>
</ScrollView>
</ScrollView>
</KeyboardAvoidingView>
<ModalSelect
category={valSelect}

View File

@@ -12,6 +12,7 @@ import { apiGetProfile } from "@/lib/api";
import { router, Stack, useLocalSearchParams } from "expo-router";
import { useEffect, useState } from "react";
import { RefreshControl, SafeAreaView, ScrollView, View } from "react-native";
import Toast from "react-native-toast-message";
import { useSelector } from "react-redux";
type Props = {
@@ -42,10 +43,15 @@ export default function MemberDetail() {
try {
setLoading(loading)
const response = await apiGetProfile({ id: id })
setData(response.data)
setEdit(valueRoleUser.filter((v) => v.login == entityUser.role)[0]?.data.some((i: any) => i.id == response.data.idUserRole))
if (response.success) {
setData(response.data)
setEdit(valueRoleUser.filter((v) => v.login == entityUser.role)[0]?.data.some((i: any) => i.id == response.data.idUserRole))
} else {
Toast.show({ type: 'small', text1: response.message })
}
} catch (error) {
console.error(error)
Toast.show({ type: 'small', text1: 'Gagal mengambil data' })
} finally {
setLoading(false)
}
@@ -84,7 +90,7 @@ export default function MemberDetail() {
/>
}
>
<View style={[Styles.wrapHeadViewMember,]}>
<View style={[Styles.wrapHeadViewMember]}>
{
loading ?
<>

View File

@@ -7,6 +7,7 @@ import Text from "@/components/Text";
import { ColorsStatus } from "@/constants/ColorsStatus";
import Styles from "@/constants/Styles";
import { apiCreateUser } from "@/lib/api";
import { validateName } from "@/lib/fun_validateName";
import { setUpdateMember } from "@/lib/memberSlice";
import { useAuthSession } from "@/providers/AuthProvider";
import { MaterialCommunityIcons } from "@expo/vector-icons";
@@ -100,7 +101,7 @@ export default function CreateMember() {
}
} else if (cat == "name") {
setDataForm({ ...dataForm, name: val });
if (val == "") {
if (!validateName(val)) {
setError({ ...error, name: true });
} else {
setError({ ...error, name: false });
@@ -166,8 +167,8 @@ export default function CreateMember() {
if (imgForm != undefined) {
fd.append("file", {
uri: imgForm.uri,
type: imgForm.mimeType,
name: imgForm.fileName,
type: imgForm.mimeType || "image/jpeg",
name: imgForm.fileName || "image.jpg",
} as any)
} else {
fd.append("file", "undefined")
@@ -193,9 +194,9 @@ export default function CreateMember() {
const pickImageAsync = async () => {
let result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ["images"],
allowsEditing: false,
quality: 1,
aspect: [1, 1],
allowsEditing: true,
quality: 0.9,
aspect: [1, 1]
});
if (!result.canceled) {
@@ -309,7 +310,7 @@ export default function CreateMember() {
placeholder="Nama"
required
error={error.name}
errorText="Nama tidak boleh kosong"
errorText="Nama harus 350 karakter (huruf, angka, spasi, dan simbol ringan (. , ' _ -))"
onChange={val => {
validationForm("name", val)
}}

View File

@@ -7,6 +7,7 @@ import Text from "@/components/Text";
import { ConstEnv } from "@/constants/ConstEnv";
import Styles from "@/constants/Styles";
import { apiEditUser, apiGetProfile } from "@/lib/api";
import { validateName } from "@/lib/fun_validateName";
import { setUpdateMember } from "@/lib/memberSlice";
import { useAuthSession } from "@/providers/AuthProvider";
import { MaterialCommunityIcons } from "@expo/vector-icons";
@@ -132,7 +133,7 @@ export default function EditMember() {
}
} else if (cat == "name") {
setData({ ...data, name: val });
if (val == "") {
if (!validateName(val)) {
setError({ ...error, name: true });
} else {
setError({ ...error, name: false });
@@ -187,8 +188,8 @@ export default function EditMember() {
if (imgForm != undefined) {
fd.append("file", {
uri: imgForm.uri,
type: imgForm.mimeType,
name: imgForm.fileName,
type: imgForm.mimeType || "image/jpeg",
name: imgForm.fileName || "image.jpg",
} as any);
} else {
fd.append("file", "undefined",);
@@ -220,9 +221,9 @@ export default function EditMember() {
const pickImageAsync = async () => {
let result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ["images"],
allowsEditing: false,
quality: 1,
aspect: [1, 1],
allowsEditing: true,
quality: 0.9,
aspect: [1, 1]
});
if (!result.canceled) {
@@ -348,7 +349,7 @@ export default function EditMember() {
required
value={data?.name}
error={error.name}
errorText="Nama tidak boleh kosong"
errorText="Nama harus 350 karakter (huruf, angka, spasi, dan simbol ringan (. , ' _ -))"
onChange={val => {
validationForm("name", val)
}}

View File

@@ -189,7 +189,10 @@ export default function Index() {
return (
<BorderBottomItem
key={index}
onPress={() => { handleChooseData(item.id, item.name, item.isActive, item.idGroup) }}
onPress={() => {
entityUser.role != "user" &&
handleChooseData(item.id, item.name, item.isActive, item.idGroup)
}}
borderType="all"
icon={
<View style={[Styles.iconContent, ColorsStatus.lightGreen]}>

View File

@@ -12,7 +12,7 @@ import { useAuthSession } from "@/providers/AuthProvider";
import { AntDesign } from "@expo/vector-icons";
import { router, Stack, useLocalSearchParams } from "expo-router";
import { useEffect, useState } from "react";
import { Pressable, SafeAreaView, ScrollView, View } from "react-native";
import { Pressable, ScrollView, View } from "react-native";
import Toast from "react-native-toast-message";
import { useDispatch, useSelector } from "react-redux";
@@ -94,7 +94,7 @@ export default function AddMemberProject() {
return (
<SafeAreaView>
<>
<Stack.Screen
options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
@@ -111,7 +111,7 @@ export default function AddMemberProject() {
)
}}
/>
<View style={[Styles.p15, Styles.mb100]}>
<View style={[Styles.p15, { flex: 1 }]}>
<InputSearch onChange={(val) => handleSearch(val)} value={search} />
{
selectMember.length > 0
@@ -176,6 +176,6 @@ export default function AddMemberProject() {
}
</ScrollView>
</View>
</SafeAreaView>
</>
)
}

View File

@@ -71,7 +71,7 @@ export default function AddMemberCreateProject() {
return (
<SafeAreaView>
<>
<Stack.Screen
options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
@@ -88,7 +88,7 @@ export default function AddMemberCreateProject() {
)
}}
/>
<View style={[Styles.p15]}>
<View style={[Styles.p15, { flex: 1 }]}>
<InputSearch onChange={(val) => setSearch(val)} value={search} />
{
@@ -145,6 +145,6 @@ export default function AddMemberCreateProject() {
}
</ScrollView>
</View>
</SafeAreaView>
</>
)
}

View File

@@ -99,14 +99,19 @@ export default function CreateProjectAddTask() {
timeStart: item.timeStart,
timeEnd: item.timeEnd,
}))
dispatch(setTaskCreate([...taskCreate, {
const hasilOrder = [...taskCreate, {
title: title,
dateStart: from,
dateEnd: to,
dateStartFix: formatDateOnly(range.startDate, "YYYY-MM-DD"),
dateEndFix: formatDateOnly(range.endDate, "YYYY-MM-DD"),
dataDetail: dataDetailFix
}]))
}].sort((a, b) => {
return new Date(a.dateStartFix).getTime() - new Date(b.dateStartFix).getTime();
});
dispatch(setTaskCreate(hasilOrder))
router.back();
} catch (error) {
console.error(error);

View File

@@ -193,16 +193,19 @@ export default function ListProject() {
</Pressable>
</View>
<View style={[Styles.mv05]}>
<Text>Filter :
{
(entityUser.role == "supadmin" || entityUser.role == "developer") && nameGroup
}
{
(entityUser.role == 'user' || entityUser.role == 'coadmin' || entityUser.role == 'cosupadmin')
? (cat == 'null' || cat == 'undefined' || cat == undefined || cat == '' || cat == 'data-saya') ? 'Kegiatan Saya' : 'Semua Kegiatan'
: ''
}
</Text>
{
entityUser.role != 'cosupadmin' && entityUser.role != 'admin' &&
<Text>Filter :
{
(entityUser.role == "supadmin" || entityUser.role == "developer") && nameGroup
}
{
(entityUser.role == 'user' || entityUser.role == 'coadmin')
? (cat == 'null' || cat == 'undefined' || cat == undefined || cat == '' || cat == 'data-saya') ? 'Kegiatan Saya' : 'Semua Kegiatan'
: ''
}
</Text>
}
</View>
</View>
<View style={[{ flex: 2 }]}>

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

@@ -50,7 +50,7 @@ export default function ViewLogin({ onValidate }: Props) {
return (
<SafeAreaView>
<StatusBar style={Platform.OS === 'ios' ? 'auto' : 'light'} translucent={false} backgroundColor="black" />
<StatusBar style={Platform.OS === 'ios' ? 'dark' : 'light'} translucent={false} backgroundColor="black" />
<ToastCustom />
<View style={[Styles.p20, Styles.h100]}>
<View style={{ alignItems: "center", marginTop: 70, marginBottom: 50 }}>

View File

@@ -57,7 +57,7 @@ export default function ViewVerification({ phone, otp }: Props) {
return (
<>
<StatusBar style={Platform.OS === 'ios' ? 'auto' : 'light'} translucent={false} backgroundColor="black" />
<StatusBar style={Platform.OS === 'ios' ? 'dark' : 'light'} translucent={false} backgroundColor="black" />
<ToastCustom />
<View style={Styles.wrapLogin} >
<View style={{ alignItems: "center", marginTop: 70, marginBottom: 50 }}>

View File

@@ -1,6 +1,6 @@
import { ColorsStatus } from "@/constants/ColorsStatus";
import Styles from "@/constants/Styles";
import React from "react";
import React, { useState } from "react";
import { Dimensions, Pressable, View } from "react-native";
import Text from "./Text";
@@ -11,6 +11,7 @@ type Props = {
desc?: string
rightTopInfo?: string
onPress?: () => void
onLongPress?: () => void
borderType: 'all' | 'bottom' | 'none'
leftBottomInfo?: React.ReactNode | string
rightBottomInfo?: React.ReactNode | string
@@ -18,21 +19,38 @@ type Props = {
bgColor?: 'white' | 'transparent'
width?: number
descEllipsize?: boolean
textColor?: string
textColor?: string,
colorPress?: boolean
titleShowAll?: boolean
}
export default function BorderBottomItem({ title, subtitle, icon, desc, onPress, rightTopInfo, borderType, leftBottomInfo, rightBottomInfo, titleWeight, bgColor, width, descEllipsize, textColor }: Props) {
export default function BorderBottomItem({ 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 style={[borderType == 'bottom' ? Styles.wrapItemBorderBottom : borderType == 'all' ? Styles.wrapItemBorderAll : Styles.wrapItemBorderNone, bgColor && bgColor == 'white' && ColorsStatus.white]} onPress={onPress}>
<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={1} ellipsizeMode='tail'>{title}</Text>
<Text style={[titleWeight == 'normal' ? Styles.textDefault : Styles.textDefaultSemiBold, { color: textColorFix }]} numberOfLines={titleShowAll ? 0 : 1} ellipsizeMode='tail'>{title}</Text>
{
subtitle &&
typeof subtitle == "string"
@@ -52,16 +70,16 @@ export default function BorderBottomItem({ title, subtitle, icon, desc, onPress,
{
(leftBottomInfo || rightBottomInfo) &&
(
<View style={[Styles.rowSpaceBetween, Styles.mt10]}>
<View style={[rightBottomInfo && !leftBottomInfo ? Styles.rowSpaceBetweenReverse : Styles.rowSpaceBetween, Styles.mt05]}>
{
typeof leftBottomInfo == 'string' ?
<Text style={[Styles.textInformation, Styles.cGray, { textAlign: 'left' }]}>{leftBottomInfo}</Text>
<Text style={[Styles.textInformation, Styles.cGray]}>{leftBottomInfo}</Text>
:
leftBottomInfo
}
{
typeof rightBottomInfo == 'string' ?
<Text style={[Styles.textInformation, Styles.mt05, Styles.cGray, { textAlign: 'right' }]}>{rightBottomInfo}</Text>
<Text style={[Styles.textInformation, Styles.cGray]}>{rightBottomInfo}</Text>
:
rightBottomInfo
}

View File

@@ -15,16 +15,16 @@ export default function DiscussionItem({ title, user, date, onPress }: Props) {
<Pressable style={[Styles.wrapItemDiscussion]} onPress={onPress}>
<View style={[Styles.rowItemsCenter, Styles.mb10]}>
<Ionicons name="chatbox-ellipses-outline" size={22} color="black" style={Styles.mr10} />
<View style={[{flex:1}]}>
<View style={[{ flex: 1 }]}>
<Text style={{ fontWeight: 'bold' }} numberOfLines={1} ellipsizeMode="tail">{title}</Text>
</View>
</View>
<View style={Styles.rowSpaceBetween}>
<View style={Styles.rowItemsCenter}>
<View style={[Styles.rowSpaceBetween]}>
<View style={[Styles.rowItemsCenter, { flex: 1 }]}>
<Feather name="user" size={18} color="grey" style={Styles.mr05} />
<Text style={[Styles.textInformation]}>{user}</Text>
<Text style={[Styles.textInformation]} numberOfLines={1} ellipsizeMode="tail">{user}</Text>
</View>
<View style={Styles.rowItemsCenter}>
<View style={[Styles.rowItemsCenter, { flex: 1, justifyContent: 'flex-end' }]}>
<Feather name="clock" size={18} color="grey" style={Styles.mr05} />
<Text style={[Styles.textInformation]}>{date}</Text>
</View>

View File

@@ -16,7 +16,10 @@ export default function HeaderDiscussionGeneral() {
return (
<>
<ButtonMenuHeader onPress={() => { setVisible(true) }} />
{
entityUser.role != "user" && entityUser.role != "coadmin" &&
<ButtonMenuHeader onPress={() => { setVisible(true) }} />
}
<DrawerBottom animation="slide" isVisible={isVisible} setVisible={setVisible} title="Menu">
<View style={Styles.rowItemsCenter}>
<MenuItemRow

View File

@@ -15,7 +15,7 @@ import { InputForm } from "../inputForm";
import MenuItemRow from "../menuItemRow";
import ModalFloat from "../modalFloat";
export default function HeaderRightDocument({ path }: { path: string }) {
export default function HeaderRightDocument({ path, isMember }: { path: string, isMember: boolean }) {
const [isVisible, setVisible] = useState(false);
const [newFolder, setNewFolder] = useState(false);
const { id } = useLocalSearchParams<{ id: string }>();
@@ -25,6 +25,7 @@ export default function HeaderRightDocument({ path }: { path: string }) {
const update = useSelector((state: any) => state.dokumenUpdate)
const [loading, setLoading] = useState(false)
const [loadingFolder, setLoadingFolder] = useState(false)
const entityUser = useSelector((state: any) => state.user)
async function handleCreateFolder() {
try {
@@ -102,11 +103,14 @@ export default function HeaderRightDocument({ path }: { path: string }) {
return (
<>
<ButtonMenuHeader
onPress={() => {
setVisible(true);
}}
/>
{
((entityUser.role != "user" && entityUser.role != "coadmin") || isMember) &&
<ButtonMenuHeader
onPress={() => {
setVisible(true);
}}
/>
}
<DrawerBottom
animation="slide"
isVisible={isVisible}

View File

@@ -11,9 +11,10 @@ type Props = {
checked?: boolean
onChecked?: () => void
onPress?: () => void
canChecked?: boolean
}
export default function ItemFile({ category, checked, dateTime, title, onChecked, onPress }: Props) {
export default function ItemFile({ category, checked, dateTime, title, onChecked, onPress, canChecked }: Props) {
return (
<View style={[Styles.wrapItemBorderBottom]}>
<View style={[Styles.rowItemsCenter]}>
@@ -43,18 +44,22 @@ export default function ItemFile({ category, checked, dateTime, title, onChecked
</Pressable>
<View style={[Styles.rowSpaceBetween, { flex: 1, alignItems: 'center' }]}>
<Pressable style={[Styles.ml10, {flex:1},]} onPress={onPress}>
<Pressable style={[Styles.ml10, { flex: 1 },]} onPress={onPress}>
<Text style={[Styles.textDefault]} numberOfLines={1} ellipsizeMode="tail">{title}</Text>
<Text style={[Styles.textInformation]}>{dateTime}</Text>
</Pressable>
<Pressable onPress={onChecked}>
{
checked
? <MaterialCommunityIcons name="checkbox-marked-circle" size={25} color={'black'} />
: <MaterialCommunityIcons name="checkbox-blank-circle-outline" size={25} color={'#ced4da'} />
}
{
!canChecked ? <></>
:
<Pressable onPress={onChecked}>
{
checked
? <MaterialCommunityIcons name="checkbox-marked-circle" size={25} color={'black'} />
: <MaterialCommunityIcons name="checkbox-blank-circle-outline" size={25} color={'#ced4da'} />
}
</Pressable>
</Pressable>
}
</View>
</View>
</View>

View File

@@ -35,6 +35,7 @@ export default function DrawerBottom({ isVisible, setVisible, title, children, a
backdropTransitionInTiming={500}
backdropTransitionOutTiming={500}
useNativeDriverForBackdrop={true}
propagateSwipe={true}
>
{
keyboard ?
@@ -62,7 +63,7 @@ export default function DrawerBottom({ isVisible, setVisible, title, children, a
<MaterialIcons name="close" color="black" size={22} />
</Pressable>
</View>
<View style={Styles.contentContainer}>
<View style={[Styles.contentContainer, { flex: 1 }]}>
{children}
</View>
</View>

View File

@@ -15,7 +15,7 @@ export default function EventItem({ category, title, user, jamAwal, jamAkhir, on
return (
<Pressable style={[Styles.itemEvent, { backgroundColor: category == 'orange' ? '#FED6C5' : '#D8D8F1' }]} onPress={onPress}>
<View style={[Styles.dividerEvent, { backgroundColor: category == 'orange' ? '#FB804C' : '#535FCA' }]} />
<View>
<View style={[Styles.w90]}>
<Text>{jamAwal} - {jamAkhir}</Text>
<Text numberOfLines={1} ellipsizeMode="tail" style={[Styles.textDefaultSemiBold, Styles.mv05]}>{title}</Text>
<Text numberOfLines={1} ellipsizeMode="tail">Dibuat oleh : {user}</Text>

View File

@@ -62,7 +62,7 @@ export default function ProjectHome({ refreshing }: { refreshing: boolean }) {
width={width * 0.8}
height={235}
data={data}
loop={true}
loop={false}
autoPlay={false}
autoPlayReverse={false}
pagingEnabled={true}

View File

@@ -104,6 +104,7 @@ export function InputDate({ label, value, placeholder, onChange, info, disable,
display="spinner"
onChange={(event, date) => { onChangeDate(event.type, date) }}
onTouchCancel={() => setModal(false)}
textColor="black"
/>
</ModalFloat>
)

View File

@@ -20,10 +20,11 @@ type Props = {
disable?: boolean
multiline?: boolean
mb?: boolean
focus?: boolean
};
export function InputForm({ label, value, placeholder, onChange, info, disable, error, errorText, required, itemLeft, itemRight, type, round, width, bg, multiline, mb = true }: Props) {
export function InputForm({ label, value, placeholder, onChange, info, disable, error, errorText, required, itemLeft, itemRight, type, round, width, bg, multiline, mb = true, focus }: Props) {
const lebar = Dimensions.get("window").width;
if (itemLeft != undefined || itemRight != undefined) {
@@ -40,10 +41,15 @@ export function InputForm({ label, value, placeholder, onChange, info, disable,
<View style={[
Styles.inputRoundForm,
itemRight != undefined ? Styles.inputRoundFormRight : Styles.inputRoundFormLeft,
multiline && { alignItems: 'flex-end' },
round && Styles.round30,
{ backgroundColor: bg && bg == 'white' ? 'white' : 'transparent' },
error && { borderColor: "red" },
Platform.OS == 'ios' ? { paddingVertical: 10 } : { paddingVertical: 0 },
Platform.OS == 'ios' ? { paddingVertical: 10 } : { paddingVertical: 0, minHeight: 40 },
{ alignItems: 'center' },
multiline
? { alignItems: "flex-end" } // multiline: tombol send di bawah
: { alignItems: "center" }, // default: tetap di tengah
]}>
{itemRight != undefined ? itemRight : itemLeft}
<TextInput
@@ -53,7 +59,14 @@ export function InputForm({ label, value, placeholder, onChange, info, disable,
keyboardType={type}
onChangeText={onChange}
placeholderTextColor={'gray'}
style={[Styles.mh05, { width: width ? lebar * width / 100 : lebar * 0.78, color: 'black' }, Platform.OS == 'ios' ? { paddingVertical: 1 } : { paddingVertical: 5 }]}
multiline={multiline}
numberOfLines={3}
style={[
Styles.mh05,
multiline && { height: '100%', maxHeight: 100 },
{ width: width ? lebar * width / 100 : lebar * 0.78, color: 'black' },
Platform.OS == 'ios' ? { paddingVertical: 1, paddingTop: 3 } : { paddingVertical: 0 },
]}
/>
</View>
{error && (<Text style={[Styles.textInformation, Styles.cError, Styles.mt05]}>{errorText}</Text>)}

View File

@@ -16,7 +16,10 @@ export default function HeaderMemberList() {
return (
<>
<ButtonMenuHeader onPress={() => { setVisible(true) }} />
{
entityUser.role != "user" &&
<ButtonMenuHeader onPress={() => { setVisible(true) }} />
}
<DrawerBottom animation="slide" isVisible={isVisible} setVisible={() => setVisible(false)} title="Menu">
<View style={Styles.rowItemsCenter}>
<MenuItemRow

View File

@@ -67,7 +67,7 @@ export default function HeaderRightProjectDetail({ id, status }: Props) {
return (
<>
<ButtonMenuHeader onPress={() => { setVisible(true) }} />
<DrawerBottom animation="slide" isVisible={isVisible} setVisible={setVisible} title="Menu" height={30}>
<DrawerBottom animation="slide" isVisible={isVisible} setVisible={setVisible} title="Menu" height={35}>
<View style={Styles.rowItemsCenter}>
<MenuItemRow
icon={<AntDesign name="pluscircle" color="black" size={25} />}

View File

@@ -30,16 +30,20 @@ export default function HeaderRightProjectList() {
}}
/>
}
<MenuItemRow
icon={<AntDesign name="filter" color="black" size={25} />}
title="Filter"
onPress={() => {
setVisible(false)
setTimeout(() => {
setFilter(true)
}, 600)
}}
/>
{
(entityUser.role == "user" || entityUser.role == "coadmin" || entityUser.role == "supadmin" || entityUser.role == "developer") &&
<MenuItemRow
icon={<AntDesign name="filter" color="black" size={25} />}
title="Filter"
onPress={() => {
setVisible(false)
setTimeout(() => {
setFilter(true)
}, 600)
}}
/>
}
</View>
</DrawerBottom>
<ModalFilter

View File

@@ -69,7 +69,7 @@ export default function HeaderRightTaskDetail({ id, division, status, isAdminDiv
return (
<>
<ButtonMenuHeader onPress={() => { setVisible(true) }} />
<DrawerBottom animation="slide" isVisible={isVisible} setVisible={setVisible} title="Menu" height={30}>
<DrawerBottom animation="slide" isVisible={isVisible} setVisible={setVisible} title="Menu" height={35}>
<View style={Styles.rowItemsCenter}>
<MenuItemRow
icon={<AntDesign name="pluscircle" color="black" size={25} />}

View File

@@ -26,7 +26,7 @@ type Props = {
position: string;
};
export default function SectionMemberTask({ refreshing, isMemberDivision }: { refreshing: boolean, isMemberDivision: boolean }) {
export default function SectionMemberTask({ refreshing, isAdminDivision }: { refreshing: boolean, isAdminDivision: boolean }) {
const [isModal, setModal] = useState(false);
const entityUser = useSelector((state: any) => state.user);
const { token, decryptToken } = useAuthSession();
@@ -168,7 +168,7 @@ export default function SectionMemberTask({ refreshing, isMemberDivision }: { re
{
(entityUser.role != "user" && entityUser.role != "coadmin") || isMemberDivision
(entityUser.role != "user" && entityUser.role != "coadmin") || isAdminDivision
?
<MenuItemRow
icon={

View File

@@ -31,5 +31,8 @@ export const ColorsStatus = {
},
lightRed: {
backgroundColor: '#ffcdcd'
},
pressedGray: {
backgroundColor: '#e4e4e4'
}
}

View File

@@ -0,0 +1 @@
export const regexOnlySpacesOrEnter = /^[\s\r\n]+$/;

View File

@@ -79,6 +79,12 @@ const Styles = StyleSheet.create({
mb10: {
marginBottom: 10
},
mb12: {
marginBottom: 12
},
mb13: {
marginBottom: 13
},
mb15: {
marginBottom: 15
},
@@ -181,6 +187,9 @@ const Styles = StyleSheet.create({
pv15: {
paddingVertical: 15
},
pv20: {
paddingVertical: 20
},
p15: {
padding: 15
},
@@ -241,6 +250,10 @@ const Styles = StyleSheet.create({
justifyContent: 'space-between',
flexDirection: 'row'
},
rowSpaceBetweenReverse: {
justifyContent: 'space-between',
flexDirection: 'row-reverse'
},
rowItemsCenter: {
flexDirection: 'row',
alignItems: 'center'
@@ -330,7 +343,9 @@ const Styles = StyleSheet.create({
borderRadius: 15,
backgroundColor: '#19345E',
display: 'flex',
width: '92%'
width: '92%',
resizeMode: 'stretch',
overflow: 'hidden',
},
wrapGridContent: {
shadowColor: '#171717',
@@ -460,6 +475,7 @@ const Styles = StyleSheet.create({
wrapHeadViewMember: {
backgroundColor: '#19345E',
paddingVertical: 30,
paddingHorizontal: 15,
alignItems: 'center',
borderBottomLeftRadius: 25,
borderBottomRightRadius: 25

View File

@@ -394,7 +394,7 @@
);
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
PRODUCT_BUNDLE_IDENTIFIER = mobiledarmasaba.app;
PRODUCT_NAME = Desa;
PRODUCT_NAME = "Desa";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Desa/Desa-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -429,7 +429,7 @@
);
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = mobiledarmasaba.app;
PRODUCT_NAME = Desa;
PRODUCT_NAME = "Desa";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Desa/Desa-Bridging-Header.h";
SWIFT_VERSION = 5.0;

View File

@@ -1,5 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>
<dict>
<key>aps-environment</key>
<string>development</string>
</dict>
</plist>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -19,7 +19,7 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0.2</string>
<string>2.0.4</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
@@ -39,7 +39,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>1</string>
<string>6</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSMinimumSystemVersion</key>
@@ -65,6 +65,10 @@
<array>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
</array>
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
</array>
<key>UILaunchStoryboardName</key>
<string>SplashScreen</string>
<key>UIRequiredDeviceCapabilities</key>

View File

@@ -182,6 +182,16 @@ export const apiSendDiscussionGeneralCommentar = async ({ id, data }: { id: stri
return response.data;
};
export const apiDeleteDiscussionGeneralCommentar = async ({ id, data }: { id: string, data: { user: string } }) => {
const response = await api.delete(`/mobile/discussion-general/${id}/comment`, { data })
return response.data;
};
export const apiUpdateDiscussionGeneralCommentar = async ({ id, data }: { id: string, data: { desc: string, user: string } }) => {
const response = await api.put(`/mobile/discussion-general/${id}/comment`, data)
return response.data;
};
export const apiDeleteMemberDiscussionGeneral = async (data: { user: string, idUser: string }, id: string) => {
await api.delete(`mobile/discussion-general/${id}/member`, { data }).then(response => {
@@ -425,6 +435,10 @@ export const apiCreateDivision = async (data: { data: { idGroup: string, name: s
return response.data;
};
export const apiCheckDivisionName = async (data: { data: { idGroup: string, name: string, desc: string }, user: string }) => {
const response = await api.put(`/mobile/division`, data)
return response.data;
};
export const apiGetDiscussion = async ({ user, search, division, active, page }: { user: string, search: string, division: string, active?: string, page?: number }) => {
const response = await api.get(`mobile/discussion?user=${user}&active=${active}&search=${search}&division=${division}&page=${page}`);
@@ -441,6 +455,16 @@ export const apiSendDiscussionCommentar = async ({ data, id }: { data: { user: s
return response.data;
};
export const apiEditDiscussionCommentar = async ({ data, id }: { data: { user: string, comment: string }, id: string }) => {
const response = await api.put(`/mobile/discussion/${id}/comment`, data)
return response.data;
};
export const apiDeleteDiscussionCommentar = async ({ data, id }: { data: { user: string }, id: string }) => {
const response = await api.delete(`/mobile/discussion/${id}/comment`, { data })
return response.data;
};
export const apiEditDiscussion = async ({ data, id }: { data: { user: string, desc: string }, id: string }) => {
const response = await api.post(`/mobile/discussion/${id}`, data)
return response.data;

18
lib/fun_validateName.ts Normal file
View File

@@ -0,0 +1,18 @@
/**
* Validasi Display Name
* Aturan:
* - 2 sampai 50 karakter
* - Huruf, angka, spasi, titik, koma, apostrof, underscore, dan dash
* - Tidak boleh semua spasi
*/
export const validateName = (name: string): boolean => {
const trimmed = name.trim();
// Jika kosong setelah di-trim → invalid
if (!trimmed) return false;
// Regex: hanya huruf, angka, spasi, titik, koma, apostrof, underscore, dash
const regex = /^[a-zA-Z0-9\s._,'-]{3,50}$/;
return regex.test(trimmed);
};

View File

@@ -1,10 +1,14 @@
import { getApp, getApps, initializeApp } from '@react-native-firebase/app';
import { getMessaging, registerDeviceForRemoteMessages, setAutoInitEnabled } from '@react-native-firebase/messaging';
import {
getMessaging,
getToken as getMessagingToken,
setAutoInitEnabled,
} from '@react-native-firebase/messaging';
import * as Notifications from 'expo-notifications';
import { useEffect } from 'react';
import { PermissionsAndroid, Platform } from 'react-native';
// Your Firebase project configuration
// Firebase config
const RNfirebaseConfig = {
apiKey: "AIzaSyB2hbsW91J3oRQx4_jgrCCNY0tNt5-21e8",
authDomain: "googleapis.com",
@@ -15,14 +19,15 @@ const RNfirebaseConfig = {
databaseURL: "https://mobile-darmasaba-default-rtdb.asia-southeast1.firebasedatabase.app/"
};
const initializeFirebase = async () => {
try {
const app = getApps().length ? getApp() : initializeApp(RNfirebaseConfig);
const mess = getMessaging(app);
await registerDeviceForRemoteMessages(mess);
setAutoInitEnabled(mess, true);
return mess
// await registerDeviceForRemoteMessages(mess);
// `registerDeviceForRemoteMessages` tidak perlu lagi
await setAutoInitEnabled(mess, true);
return mess;
} catch (error) {
console.error('Failed to initialize Firebase:', error);
}
@@ -31,17 +36,16 @@ const initializeFirebase = async () => {
export const requestPermission = async () => {
try {
if (Platform.OS === 'android') {
const cek = await PermissionsAndroid.check(PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS)
const cek = await PermissionsAndroid.check(
PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS
);
if (!cek) {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS
);
if (granted === PermissionsAndroid.RESULTS.GRANTED) {
return true
}
return false
return granted === PermissionsAndroid.RESULTS.GRANTED;
}
return true
return true;
} else if (Platform.OS === 'ios') {
const { status } = await Notifications.requestPermissionsAsync();
return status === 'granted';
@@ -54,10 +58,13 @@ export const requestPermission = async () => {
export const getToken = async () => {
try {
const mess = await initializeFirebase();
const token = await mess?.getToken();
if (!mess) return null;
// pakai modular API
const token = await getMessagingToken(mess);
return token;
} catch (error) {
console.error("Error getting token:", error);
console.error('Error getting token:', error);
}
};
@@ -73,4 +80,4 @@ export const useNotification = () => {
initializeAndSetup();
}, []);
};
};

View File

@@ -5,7 +5,6 @@ import AsyncStorage from '@react-native-async-storage/async-storage';
import CryptoES from "crypto-es";
import { router } from "expo-router";
import { createContext, MutableRefObject, ReactNode, useCallback, useContext, useEffect, useRef, useState } from 'react';
import { Platform } from 'react-native';
const AuthContext = createContext<{
signIn: (arg0: string) => void;
@@ -57,13 +56,13 @@ export default function AuthProvider({ children }: { children: ReactNode }): Rea
const permission = await requestPermission()
if (permission) {
try {
// COMING SOON
if (Platform.OS === 'android') {
const tokenDevice = await getToken()
const register = await apiRegisteredToken({ user: hasil, token: String(tokenDevice) })
}else{
const register = await apiRegisteredToken({ user: hasil, token: "" })
}
// if (Platform.OS === 'android') {
const tokenDevice = await getToken()
const register = await apiRegisteredToken({ user: hasil, token: String(tokenDevice) })
// }else{
// const tokenDevice = await getToken()
// const register = await apiRegisteredToken({ user: hasil, token: String(tokenDevice) })
// }
} catch (error) {
console.error(error)
} finally {
@@ -82,13 +81,12 @@ export default function AuthProvider({ children }: { children: ReactNode }): Rea
const signOut = useCallback(async () => {
try {
const hasil = await decryptToken(String(tokenRef.current))
// COMING SOON
if (Platform.OS === 'android') {
const token = await getToken()
const response = await apiUnregisteredToken({ user: hasil, token: String(token) })
}else{
const response = await apiUnregisteredToken({ user: hasil, token: "" })
}
// if (Platform.OS === 'android') {
const token = await getToken()
const response = await apiUnregisteredToken({ user: hasil, token: String(token) })
// }else{
// const response = await apiUnregisteredToken({ user: hasil, token: "" })
// }
} catch (error) {
console.error(error)
} finally {