Merge pull request 'amalia/03-okt-25' (#45) from amalia/03-okt-25 into join

Reviewed-on: bip/mobile-darmasaba#45
This commit is contained in:
2025-10-03 17:31:32 +08:00
11 changed files with 170 additions and 129 deletions

View File

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

View File

@@ -45,6 +45,8 @@ export default function AddMemberDivision() {
setData(responsemember.data.filter((i: any) => i.idUserRole != 'supadmin')) setData(responsemember.data.filter((i: any) => i.idUserRole != 'supadmin'))
} catch (error) { } catch (error) {
console.error(error) console.error(error)
} finally {
setLoading(false)
} }
} }

View File

@@ -16,7 +16,7 @@ import { useAuthSession } from "@/providers/AuthProvider"
import { Feather, MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons" import { Feather, MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons"
import { router, Stack, useLocalSearchParams } from "expo-router" import { router, Stack, useLocalSearchParams } from "expo-router"
import { useEffect, useState } from "react" 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 Toast from "react-native-toast-message"
import { useSelector } from "react-redux" import { useSelector } from "react-redux"
@@ -39,6 +39,7 @@ type PropsMember = {
} }
export default function InformationDivision() { export default function InformationDivision() {
const [refreshing, setRefreshing] = useState(false)
const entityUser = useSelector((state: any) => state.user) const entityUser = useSelector((state: any) => state.user)
const { id } = useLocalSearchParams<{ id: string }>() const { id } = useLocalSearchParams<{ id: string }>()
const [isModal, setModal] = useState(false) 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() { async function handleCheckMember() {
try { try {
const hasil = await decryptToken(String(token?.current)); 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} />, 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]}> <View style={[Styles.p15]}>
{ {
dataDetail?.isActive == false && ( dataDetail?.isActive == false && (

View File

@@ -8,6 +8,7 @@ import { ConstEnv } from "@/constants/ConstEnv";
import Styles from "@/constants/Styles"; import Styles from "@/constants/Styles";
import { apiEditProfile, apiGetProfile } from "@/lib/api"; import { apiEditProfile, apiGetProfile } from "@/lib/api";
import { setEntities } from "@/lib/entitiesSlice"; import { setEntities } from "@/lib/entitiesSlice";
import { validateName } from "@/lib/fun_validateName";
import { useAuthSession } from "@/providers/AuthProvider"; import { useAuthSession } from "@/providers/AuthProvider";
import { MaterialCommunityIcons } from "@expo/vector-icons"; import { MaterialCommunityIcons } from "@expo/vector-icons";
import { useHeaderHeight } from "@react-navigation/elements"; import { useHeaderHeight } from "@react-navigation/elements";
@@ -109,7 +110,7 @@ export default function EditProfile() {
} }
} else if (cat == "name") { } else if (cat == "name") {
setData({ ...data, name: val }); setData({ ...data, name: val });
if (val == "") { if (!validateName(val)) {
setError({ ...error, name: true }); setError({ ...error, name: true });
} else { } else {
setError({ ...error, name: false }); setError({ ...error, name: false });
@@ -164,8 +165,8 @@ export default function EditProfile() {
if (imgForm != undefined) { if (imgForm != undefined) {
fd.append("file", { fd.append("file", {
uri: imgForm.uri, uri: imgForm.uri,
type: imgForm.mimeType, type: imgForm.mimeType || "image/jpeg",
name: imgForm.fileName, name: imgForm.fileName || "image.jpg",
} as any); } as any);
} else { } else {
fd.append("file", "undefined",); fd.append("file", "undefined",);
@@ -197,9 +198,9 @@ export default function EditProfile() {
const pickImageAsync = async () => { const pickImageAsync = async () => {
let result = await ImagePicker.launchImageLibraryAsync({ let result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ["images"], mediaTypes: ["images"],
allowsEditing: false, allowsEditing: true,
quality: 1, quality: 0.9,
aspect: [1, 1], aspect: [1, 1]
}); });
if (!result.canceled) { if (!result.canceled) {
@@ -305,7 +306,7 @@ export default function EditProfile() {
required required
value={data?.name} value={data?.name}
error={error.name} error={error.name}
errorText="Nama tidak boleh kosong" errorText="Nama harus 350 karakter (huruf, angka, spasi, dan simbol ringan (. , ' _ -))"
onChange={val => { onChange={val => {
validationForm("name", val) validationForm("name", val)
}} }}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,99 +1,99 @@
<?xml version="1.0" encoding="UTF-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"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<true/> <true/>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>Desa+</string> <string>Desa+</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string> <string>6.0</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string> <string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string> <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.0.5</string> <string>2.0.1</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleURLTypes</key> <key>CFBundleURLTypes</key>
<array> <array>
<dict> <dict>
<key>CFBundleURLSchemes</key> <key>CFBundleURLSchemes</key>
<array> <array>
<string>myapp</string> <string>myapp</string>
<string>mobiledarmasaba.app</string> <string>mobiledarmasaba.app</string>
</array> </array>
</dict> </dict>
<dict> <dict>
<key>CFBundleURLSchemes</key> <key>CFBundleURLSchemes</key>
<array> <array>
<string>exp+mobile-darmasaba</string> <string>exp+mobile-darmasaba</string>
</array> </array>
</dict> </dict>
</array> </array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>2</string> <string>3</string>
<key>ITSAppUsesNonExemptEncryption</key> <key>ITSAppUsesNonExemptEncryption</key>
<false/> <false/>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>
<string>12.0</string> <string>12.0</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>NSAppTransportSecurity</key> <key>NSAppTransportSecurity</key>
<dict> <dict>
<key>NSAllowsArbitraryLoads</key> <key>NSAllowsArbitraryLoads</key>
<false/> <false/>
<key>NSAllowsLocalNetworking</key> <key>NSAllowsLocalNetworking</key>
<true/> <true/>
</dict> </dict>
<key>NSCameraUsageDescription</key> <key>NSCameraUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your camera</string> <string>Allow $(PRODUCT_NAME) to access your camera</string>
<key>NSMicrophoneUsageDescription</key> <key>NSMicrophoneUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your microphone</string> <string>Allow $(PRODUCT_NAME) to access your microphone</string>
<key>NSPhotoLibraryAddUsageDescription</key> <key>NSPhotoLibraryAddUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to save photos</string> <string>Allow $(PRODUCT_NAME) to save photos</string>
<key>NSPhotoLibraryUsageDescription</key> <key>NSPhotoLibraryUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your photos</string> <string>Allow $(PRODUCT_NAME) to access your photos</string>
<key>NSUserActivityTypes</key> <key>NSUserActivityTypes</key>
<array> <array>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string> <string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
</array> </array>
<key>UIBackgroundModes</key> <key>UIBackgroundModes</key>
<array> <array>
<string>remote-notification</string> <string>remote-notification</string>
</array> </array>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>SplashScreen</string> <string>SplashScreen</string>
<key>UIRequiredDeviceCapabilities</key> <key>UIRequiredDeviceCapabilities</key>
<array> <array>
<string>arm64</string> <string>arm64</string>
</array> </array>
<key>UIRequiresFullScreen</key> <key>UIRequiresFullScreen</key>
<false/> <false/>
<key>UIStatusBarStyle</key> <key>UIStatusBarStyle</key>
<string>UIStatusBarStyleDefault</string> <string>UIStatusBarStyleDefault</string>
<key>UISupportedInterfaceOrientations</key> <key>UISupportedInterfaceOrientations</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string> <string>UIInterfaceOrientationPortraitUpsideDown</string>
</array> </array>
<key>UISupportedInterfaceOrientations~ipad</key> <key>UISupportedInterfaceOrientations~ipad</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string> <string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>UIUserInterfaceStyle</key> <key>UIUserInterfaceStyle</key>
<string>Automatic</string> <string>Automatic</string>
<key>UIViewControllerBasedStatusBarAppearance</key> <key>UIViewControllerBasedStatusBarAppearance</key>
<false/> <false/>
</dict> </dict>
</plist> </plist>

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