Compare commits

...

25 Commits

Author SHA1 Message Date
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
35 changed files with 339 additions and 200 deletions

View File

@@ -4,7 +4,7 @@ export default {
expo: {
name: "Desa+",
slug: "mobile-darmasaba",
version: "1.0.5", // Versi aplikasi (App Store)
version: "2.0.1", // 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: "2",
buildNumber: "3",
infoPlist: {
ITSAppUsesNonExemptEncryption: false,
CFBundleDisplayName: "Desa+"
@@ -23,7 +23,7 @@ export default {
},
android: {
package: "mobiledarmasaba.app",
versionCode: 9,
versionCode: 10,
adaptiveIcon: {
foregroundImage: "./assets/images/logo-icon-small.png",
backgroundColor: "#ffffff"

View File

@@ -19,6 +19,7 @@ import { getMessaging, onMessage } from "@react-native-firebase/messaging";
import { Redirect, router, Stack, usePathname } from "expo-router";
import { StatusBar } from 'expo-status-bar';
import { useEffect } from "react";
import { Platform } from "react-native";
import { Easing, Notifier } from 'react-native-notifier';
import { Provider } from "react-redux";
@@ -148,7 +149,7 @@ export default function RootLayout() {
}}
/>
</Stack>
<StatusBar style="inverted" translucent={false} backgroundColor="black" />
<StatusBar style={'light'} translucent={false} backgroundColor="black" />
<ToastCustom />
</Provider>
)

View File

@@ -9,6 +9,7 @@ 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 { getDB } from "@/lib/firebaseDatabase";
@@ -160,7 +161,6 @@ export default function DetailDiscussionGeneral() {
:
<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]}>
@@ -222,10 +222,14 @@ export default function DetailDiscussionGeneral() {
multiline
itemRight={
<Pressable onPress={() => {
(komentar != '' && data?.status === 1 && data?.isActive && (memberDiscussion || (entityUser.role != "user" && entityUser.role != "coadmin")))
(komentar != '' && !regexOnlySpacesOrEnter.test(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} />
}}
style={[
Platform.OS == 'android' && Styles.mb12,
]}
>
<MaterialIcons name="send" size={25} style={(komentar == '' || regexOnlySpacesOrEnter.test(komentar) || data?.status === 2 || !data?.isActive || (!memberDiscussion && (entityUser.role == "user" || entityUser.role == "coadmin"))) ? Styles.cGray : Styles.cDefault} />
</Pressable>
}
/>

View File

@@ -8,6 +8,7 @@ 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 {
apiGetDiscussionOne,
@@ -312,6 +313,7 @@ export default function DiscussionDetail() {
<Pressable
onPress={() => {
komentar != "" &&
!regexOnlySpacesOrEnter.test(komentar) &&
!loadingSend &&
data?.status != 2 &&
data?.isActive &&
@@ -324,12 +326,16 @@ export default function DiscussionDetail() {
entityUser.role == "cosupadmin") &&
handleKomentar();
}}
style={[
Platform.OS == 'android' && Styles.mb12,
]}
>
<MaterialIcons
name="send"
size={25}
style={
komentar == "" ||
[komentar == "" ||
regexOnlySpacesOrEnter.test(komentar) ||
loadingSend ||
data?.status == 2 ||
data?.isActive == false ||
@@ -337,7 +343,8 @@ export default function DiscussionDetail() {
entityUser.role == "coadmin") &&
!isMemberDivision)
? Styles.cGray
: Styles.cDefault
: Styles.cDefault,
]
}
/>
</Pressable>

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

@@ -16,7 +16,7 @@ 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,7 @@ 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)
@@ -116,6 +117,14 @@ 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));
@@ -161,7 +170,15 @@ export default function InformationDivision() {
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 && (

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,35 @@ 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',
desc: 'Nama divisi sudah ada. Apakah anda yakin ingin membuat divisi dengan nama yang sama?',
onPress: () => {
handleSetData()
}
})
} 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 +114,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

@@ -74,7 +74,7 @@ export default function CreateDivisionAddAdmin() {
return (
<SafeAreaView>
<>
<Stack.Screen
options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
@@ -91,7 +91,7 @@ export default function CreateDivisionAddAdmin() {
)
}}
/>
<View style={[Styles.p15]}>
<View style={[Styles.p15, { flex: 1 }]}>
<ScrollView>
{
data.length > 0 ?
@@ -126,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,6 +8,7 @@ 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";
@@ -109,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 });
@@ -164,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",);
@@ -197,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) {
@@ -305,7 +306,7 @@ export default function EditProfile() {
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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -44,7 +44,11 @@ export function InputForm({ label, value, placeholder, onChange, info, disable,
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
@@ -58,12 +62,9 @@ export function InputForm({ label, value, placeholder, onChange, info, disable,
numberOfLines={3}
style={[
Styles.mh05,
multiline && { height: '100%', paddingTop: 3 },
multiline && { height: '100%', maxHeight: 100 },
{ width: width ? lebar * width / 100 : lebar * 0.78, color: 'black' },
Platform.OS == 'ios' ? { paddingVertical: 1 } : { paddingVertical: 5 },
multiline && {
maxHeight: 100, // batas maksimal
}
Platform.OS == 'ios' ? { paddingVertical: 1, paddingTop: 3 } : { paddingVertical: 0 },
]}
/>
</View>

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

@@ -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
},

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,8 +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>
<key>aps-environment</key>
<string>development</string>
</dict>
</plist>
<dict>
<key>aps-environment</key>
<string>development</string>
</dict>
</plist>

View File

@@ -1,99 +1,99 @@
<?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>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Desa+</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0.5</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
<string>mobiledarmasaba.app</string>
</array>
</dict>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>exp+mobile-darmasaba</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>2</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSMinimumSystemVersion</key>
<string>12.0</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<false/>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
<key>NSCameraUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your camera</string>
<key>NSMicrophoneUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your microphone</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to save photos</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your photos</string>
<key>NSUserActivityTypes</key>
<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>
<array>
<string>arm64</string>
</array>
<key>UIRequiresFullScreen</key>
<false/>
<key>UIStatusBarStyle</key>
<string>UIStatusBarStyleDefault</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIUserInterfaceStyle</key>
<string>Automatic</string>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Desa+</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>2.0.1</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
<string>mobiledarmasaba.app</string>
</array>
</dict>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>exp+mobile-darmasaba</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>3</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSMinimumSystemVersion</key>
<string>12.0</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<false/>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
<key>NSCameraUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your camera</string>
<key>NSMicrophoneUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your microphone</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to save photos</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your photos</string>
<key>NSUserActivityTypes</key>
<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>
<array>
<string>arm64</string>
</array>
<key>UIRequiresFullScreen</key>
<false/>
<key>UIStatusBarStyle</key>
<string>UIStatusBarStyleDefault</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIUserInterfaceStyle</key>
<string>Automatic</string>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>

View File

@@ -425,6 +425,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}`);

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);
};