Compare commits

...

21 Commits

Author SHA1 Message Date
9793794ff2 upd: kode verification
Deskripsi:
- update logo pda page kode otp
- update text wa

No Issues
2025-09-04 18:38:19 +08:00
d0aaa5561c upd: ios 2025-09-04 18:17:41 +08:00
90042d13dd upd: android dan ios 2025-09-04 17:46:30 +08:00
596565ba8e upd: gitignore 2025-09-04 17:33:16 +08:00
22663acaae upd: icon small 2025-09-04 17:30:02 +08:00
d3354e3e74 upd: android dan ios 2025-09-04 17:29:45 +08:00
c3ab4d05ae upd
: update icon

Deskirpsi:
- ganti logo dan icon dan nama aplikasi

Mo Issues
2025-09-04 16:46:57 +08:00
270001aa4f upd : build version
Deskripsi;
- build version otomatis

No Issues
2025-09-03 16:49:40 +08:00
1122e51047 upd: color status bar
Deskripsi:
- status bar pada login dan halaman konfirmasi otp

No Issues
2025-09-02 11:48:38 +08:00
265656413d upd: login tanpa verifikasi otp
Deskripsi:
- dibuat untuk pengecekan aplikasi oleh tim google play store

No Issues
2025-09-02 11:08:58 +08:00
db0f0ecd6c upd: version app 2025-09-02 11:07:06 +08:00
24e1ace521 fix : login
Deskripsi :
- login api

NO Issues
2025-09-01 17:05:51 +08:00
019c0a5e33 upd : coba rollout google playstore
Deskripsi :
- internal track

No Issues
2025-09-01 15:38:29 +08:00
4250ca3057 Merge pull request 'amalia/29-agustus-25' (#32) from amalia/29-agustus-25 into join
Reviewed-on: bip/mobile-darmasaba#32
2025-08-29 17:04:10 +08:00
c775b06dc3 upd: load dokumen divisi
Deskripsi:
- ilangin skeleton loading pada path load data

No Issues
2025-08-29 16:56:09 +08:00
77cd07ad7a upd: upd new folder
Deskripsi:
- bisa membuat folder baru saat salin atau pindah file pada fitur dokumen divisi

No Issues
2025-08-29 16:52:08 +08:00
c35e2e65bd upd: tambah jabatan
Deskripsi:
- pisah class modal form tambah jabatan supaya bisa double modal

No Issues
2025-08-29 15:23:08 +08:00
6a24b95cdd upd: loading button
Deskripsi:
- tambah folder bar
- rename file pada dokumen divisi

No Issues
2025-08-29 14:58:16 +08:00
92c58524f6 upd : salin link
Deskripsi:
- detail kalender event divisi > dapat di copy

No Issues
2025-08-29 12:04:58 +08:00
7d5ec511f5 upd: input date
Deskripsi:
- on submit value pada ios
- on cancel value pada android

No Issues'
2025-08-29 11:18:38 +08:00
3de8e628b6 upd: tampilan
Deskripsi:
- update tampilan select anggota

No Issues
2025-08-29 10:42:11 +08:00
76 changed files with 540 additions and 332 deletions

4
.gitignore vendored
View File

@@ -44,4 +44,6 @@ x.ts
x.sh
google-services.json
mobile-darmasaba-firebase-adminsdk-fbsvc-f5abb292b5.json
service-account.json
GoogleService-Info.plist.bak

View File

@@ -92,8 +92,8 @@ android {
applicationId 'mobiledarmasaba.app'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0.0"
versionCode 6
versionName "1.0.2"
}
signingConfigs {
debug {

View File

@@ -1,6 +1,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.VIBRATE"/>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 360 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 575 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@@ -1,5 +1,5 @@
<resources>
<string name="app_name">mobile-darmasaba</string>
<string name="app_name">Desa+</string>
<string name="expo_system_ui_user_interface_style" translatable="false">automatic</string>
<string name="expo_splash_screen_resize_mode" translatable="false">contain</string>
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>

View File

@@ -31,7 +31,7 @@ extensions.configure(com.facebook.react.ReactSettingsExtension) { ex ->
}
expoAutolinking.useExpoModules()
rootProject.name = 'mobile-darmasaba'
rootProject.name = 'Desa+'
expoAutolinking.useExpoVersionCatalog()

View File

@@ -2,12 +2,12 @@ import 'dotenv/config';
export default {
expo: {
name: "mobile-darmasaba",
name: "Desa+",
slug: "mobile-darmasaba",
version: "1.0.0",
version: "1.0.2",
jsEngine: "jsc",
orientation: "portrait",
icon: "./assets/images/icon.png",
icon: "./assets/images/logo-icon-small.png",
scheme: "myapp",
userInterfaceStyle: "automatic",
newArchEnabled: false,
@@ -15,17 +15,26 @@ export default {
supportsTablet: true,
bundleIdentifier: "mobiledarmasaba.app",
infoPlist: {
ITSAppUsesNonExemptEncryption: false
ITSAppUsesNonExemptEncryption: false,
CFBundleDisplayName: "Desa+"
},
googleServicesFile: "./ios/mobiledarmasaba/GoogleService-Info.plist"
googleServicesFile: "./ios/Desa/GoogleService-Info.plist"
},
android: {
package: "mobiledarmasaba.app",
versionCode: 6,
adaptiveIcon: {
foregroundImage: "./assets/images/splash-icon.png",
foregroundImage: "./assets/images/logo-icon-small.png",
backgroundColor: "#ffffff"
},
googleServicesFile: "./google-services.json"
googleServicesFile: "./google-services.json",
permissions: [
"READ_EXTERNAL_STORAGE",
"WRITE_EXTERNAL_STORAGE",
"READ_MEDIA_IMAGES", // Android 13+
"READ_MEDIA_VIDEO", // Android 13+
"READ_MEDIA_AUDIO" // Android 13+
]
},
web: {
bundler: "metro",
@@ -37,7 +46,7 @@ export default {
[
"expo-splash-screen",
{
image: "./assets/images/splash-icon.png",
image: "./assets/images/logo-icon-small.png",
imageWidth: 200,
resizeMode: "contain",
backgroundColor: "#ffffff"
@@ -50,7 +59,7 @@ export default {
"@react-native-firebase/app",
{
ios: {
googleServicesFile: "./ios/mobiledarmasaba/GoogleService-Info.plist"
googleServicesFile: "./ios/Desa/GoogleService-Info.plist"
}
}
]

View File

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

View File

@@ -13,9 +13,10 @@ import { apiDeleteCalendarMember, apiGetCalendarOne, apiGetDivisionOneFeature }
import { setUpdateCalendar } from "@/lib/calendarUpdate"
import { useAuthSession } from "@/providers/AuthProvider"
import { MaterialCommunityIcons } from "@expo/vector-icons"
import Clipboard from "@react-native-clipboard/clipboard"
import { router, Stack, useLocalSearchParams } from "expo-router"
import { useEffect, useState } from "react"
import { RefreshControl, SafeAreaView, ScrollView, View } from "react-native"
import { Pressable, RefreshControl, SafeAreaView, ScrollView, View } from "react-native"
import Toast from "react-native-toast-message"
import { useDispatch, useSelector } from "react-redux"
@@ -115,6 +116,11 @@ export default function DetailEventCalendar() {
handleLoadMember();
}, [update.member]);
const handleCopy = (text: string) => {
Clipboard.setString(text);
Toast.show({ type: 'small', text1: 'Berhasil menyalin link', })
};
async function handleDeleteUser() {
try {
const hasil = await decryptToken(String(token?.current));
@@ -216,10 +222,14 @@ export default function DetailEventCalendar() {
loading ?
<Skeleton width={80} height={10} borderRadius={10} widthType="percent" />
:
<Text style={[Styles.textDefault]}>{data?.linkMeet ? data.linkMeet : '-'}</Text>
data?.linkMeet ?
<Pressable onPress={() => { handleCopy(data.linkMeet) }}>
<Text style={[Styles.textDefault]}>{data.linkMeet}</Text>
</Pressable>
: <Text style={[Styles.textDefault]}>-</Text>
}
</View>
<View style={[Styles.rowItemsCenter, Styles.mt10, {alignItems:'flex-start'}]}>
<View style={[Styles.rowItemsCenter, Styles.mt10, { alignItems: 'flex-start' }]}>
<MaterialCommunityIcons name="card-text-outline" size={30} color="black" style={Styles.mr10} />
{
loading ?

View File

@@ -65,6 +65,7 @@ type PropsPath = {
};
export default function DocumentDivision() {
const [loadingRename, setLoadingRename] = useState(false)
const [isShare, setShare] = useState(false)
const { token, decryptToken } = useAuthSession()
const { id } = useLocalSearchParams<{ id: string }>()
@@ -201,6 +202,7 @@ export default function DocumentDivision() {
async function handleRename() {
try {
setLoadingRename(true)
const hasil = await decryptToken(String(token?.current));
const response = await apiDocumentRename({ user: hasil, ...bodyRename });
if (response.success) {
@@ -214,7 +216,8 @@ export default function DocumentDivision() {
console.error(error);
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
} finally {
setRename(false);
setLoadingRename(false)
setRename(false)
}
}
@@ -360,25 +363,20 @@ export default function DocumentDivision() {
<View style={[Styles.p15, Styles.mb100]}>
<View style={[Styles.rowItemsCenter]}>
{
loading ?
arrSkeleton.map((item, index) => (
<Skeleton key={index} width={60} height={10} borderRadius={10} style={[Styles.mr05]} />
))
:
dataJalur.map((item, index) => (
<Pressable
key={index}
style={[Styles.rowItemsCenter]}
onPress={() => {
setPath(item.id);
}}
>
{item.id != "home" && (
<AntDesign name="right" style={[Styles.mh05, Styles.mt02]} color="black" />
)}
<Text> {item.name} </Text>
</Pressable>
))
dataJalur.map((item, index) => (
<Pressable
key={index}
style={[Styles.rowItemsCenter]}
onPress={() => {
setPath(item.id);
}}
>
{item.id != "home" && (
<AntDesign name="right" style={[Styles.mh05, Styles.mt02]} color="black" />
)}
<Text> {item.name} </Text>
</Pressable>
))
}
</View>
<View>
@@ -538,7 +536,7 @@ export default function DocumentDivision() {
isVisible={isRename}
setVisible={() => { setRename(false) }}
onSubmit={() => { handleRename() }}
disableSubmit={bodyRename.name == ""}
disableSubmit={bodyRename.name == "" || loadingRename}
>
<View>
<InputForm

View File

@@ -27,10 +27,12 @@ export default function Index() {
<View style={Styles.wrapLogin} >
<View style={{ alignItems: "center", marginVertical: 50 }}>
<Image
source={require("../assets/images/splash-icon.png")}
style={{ width: 130, height: 130 }}
source={require("../assets/images/logo.png")}
style={[{ width: 300, height: 150 }]}
width={270}
height={110}
/>
<Text style={[Styles.textSubtitle]}>PERBEKEL DARMASABA</Text>
{/* <Text style={[Styles.textSubtitle]}>PERBEKEL DARMASABA</Text> */}
</View>
<View style={[Styles.mb30]}>
<Text style={[Styles.textDefaultSemiBold]}>Verifikasi Nomor Telepon</Text>

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
assets/images/logo-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

BIN
assets/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

38
bump-version.js Normal file
View File

@@ -0,0 +1,38 @@
const fs = require("fs");
const path = require("path");
const configPath = path.join(__dirname, "app.config.js");
let configFile = fs.readFileSync(configPath, "utf8");
// --- Update versionCode ---
const codeRegex = /versionCode:\s*(\d+)/;
const codeMatch = configFile.match(codeRegex);
if (!codeMatch) {
console.error("❌ Tidak menemukan versionCode di app.config.js");
process.exit(1);
}
const currentCode = parseInt(codeMatch[1], 10);
const newCode = currentCode + 1;
configFile = configFile.replace(codeRegex, `versionCode: ${newCode}`);
// --- Update versionName ---
const nameRegex = /version:\s*"(.*?)"/;
const nameMatch = configFile.match(nameRegex);
if (!nameMatch) {
console.error("❌ Tidak menemukan version di app.config.js");
process.exit(1);
}
let [major, minor, patch] = nameMatch[1].split(".").map(Number);
patch += 1; // bump patch version
const newName = `${major}.${minor}.${patch}`;
configFile = configFile.replace(nameRegex, `version: "${newName}"`);
// --- Simpan file ---
fs.writeFileSync(configPath, configFile, "utf8");
console.log(`✅ versionCode: ${currentCode}${newCode}`);
console.log(`✅ versionName: ${nameMatch[1]}${newName}`);

BIN
bun.lockb

Binary file not shown.

View File

@@ -1,8 +1,10 @@
import Styles from "@/constants/Styles"
import { apiCheckPhoneLogin, apiSendOtp } from "@/lib/api"
import { useAuthSession } from "@/providers/AuthProvider"
import AsyncStorage from "@react-native-async-storage/async-storage"
import { StatusBar } from "expo-status-bar"
import { useState } from "react"
import { Image, SafeAreaView, View } from "react-native"
import { Image, Platform, SafeAreaView, View } from "react-native"
import Toast from "react-native-toast-message"
import { ButtonForm } from "../buttonForm"
import { InputForm } from "../inputForm"
@@ -10,7 +12,6 @@ import ModalLoading from "../modalLoading"
import Text from "../Text"
import ToastCustom from "../toastCustom"
type Props = {
onValidate: ({ phone, otp }: { phone: string, otp: number }) => void
}
@@ -19,20 +20,27 @@ export default function ViewLogin({ onValidate }: Props) {
const [loadingLogin, setLoadingLogin] = useState(false)
const [disableLogin, setDisableLogin] = useState(true)
const [phone, setPhone] = useState('')
const { signIn, encryptToken } = useAuthSession();
const handleCheckPhone = async () => {
try {
setLoadingLogin(true)
const response = await apiCheckPhoneLogin({ phone: `62${phone}` });
const response = await apiCheckPhoneLogin({ phone: `62${phone}` })
if (response.success) {
const otp = Math.floor(1000 + Math.random() * 9000)
const responseOtp = await apiSendOtp({ phone: `62${phone}`, otp })
if (responseOtp == 200) {
await AsyncStorage.setItem('user', response.id);
return onValidate({ phone: `62${phone}`, otp })
if (response.isWithoutOTP) {
const encrypted = await encryptToken(response.id)
signIn(encrypted)
} else {
const otp = Math.floor(1000 + Math.random() * 9000)
const responseOtp = await apiSendOtp({ phone: `62${phone}`, otp })
if (responseOtp == 200) {
await AsyncStorage.setItem('user', response.id)
return onValidate({ phone: `62${phone}`, otp })
}
}
} else {
return Toast.show({ type: 'small', text1: response.message, position: 'top' })
}
return Toast.show({ type: 'small', text1: response.message, position: 'top' })
} catch (error) {
return Toast.show({ type: 'small', text1: 'Terjadi kesalahan', position: 'top' })
} finally {
@@ -42,14 +50,17 @@ export default function ViewLogin({ onValidate }: Props) {
return (
<SafeAreaView>
<StatusBar style={Platform.OS === 'ios' ? 'auto' : 'light'} translucent={false} backgroundColor="black" />
<ToastCustom />
<View style={[Styles.p20, Styles.h100]}>
<View style={{ alignItems: "center", marginVertical: 50 }}>
<View style={{ alignItems: "center", marginTop: 70, marginBottom: 50 }}>
<Image
source={require("../../assets/images/splash-icon.png")}
style={{ width: 130, height: 130 }}
source={require("../../assets/images/logo.png")}
style={[{ width: 300, height: 150 }]}
width={270}
height={110}
/>
<Text style={[Styles.textSubtitle]}>PERBEKEL DARMASABA</Text>
{/* <Text style={[Styles.textSubtitle2]}>Digitalisasi Desa Transparansi Kerja</Text> */}
</View>
<InputForm
onChange={(val) => {

View File

@@ -2,8 +2,9 @@ import Styles from "@/constants/Styles";
import { apiSendOtp } from "@/lib/api";
import { useAuthSession } from "@/providers/AuthProvider";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { StatusBar } from "expo-status-bar";
import { useState } from "react";
import { Image, View } from "react-native";
import { Image, Platform, View } from "react-native";
import { OtpInput } from "react-native-otp-entry";
import Toast from 'react-native-toast-message';
import { ButtonForm } from "../buttonForm";
@@ -56,14 +57,17 @@ export default function ViewVerification({ phone, otp }: Props) {
return (
<>
<StatusBar style={Platform.OS === 'ios' ? 'auto' : 'light'} translucent={false} backgroundColor="black" />
<ToastCustom />
<View style={Styles.wrapLogin} >
<View style={{ alignItems: "center", marginVertical: 50 }}>
<View style={{ alignItems: "center", marginTop: 70, marginBottom: 50 }}>
<Image
source={require("../../assets/images/splash-icon.png")}
style={{ width: 130, height: 130 }}
source={require("../../assets/images/logo.png")}
style={[{ width: 300, height: 150 }]}
width={270}
height={110}
/>
<Text style={[Styles.textSubtitle]}>PERBEKEL DARMASABA</Text>
{/* <Text style={[Styles.textSubtitle]}>PERBEKEL DARMASABA</Text> */}
</View>
<View style={[Styles.mb30]}>
<Text style={[Styles.textDefaultSemiBold]}>Verifikasi Nomor Telepon</Text>

View File

@@ -24,9 +24,11 @@ export default function HeaderRightDocument({ path }: { path: string }) {
const dispatch = useDispatch()
const update = useSelector((state: any) => state.dokumenUpdate)
const [loading, setLoading] = useState(false)
const [loadingFolder, setLoadingFolder] = useState(false)
async function handleCreateFolder() {
try {
setLoadingFolder(true)
const hasil = await decryptToken(String(token?.current))
const response = await apiCreateFolderDocument({ data: { user: hasil, name, path, idDivision: id } })
if (response.success) {
@@ -39,6 +41,7 @@ export default function HeaderRightDocument({ path }: { path: string }) {
console.error(error)
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
} finally {
setLoadingFolder(false)
setNewFolder(false)
}
}
@@ -148,7 +151,7 @@ export default function HeaderRightDocument({ path }: { path: string }) {
title="Buat Folder Baru"
isVisible={newFolder}
setVisible={() => { setNewFolder(false) }}
disableSubmit={name == ""}
disableSubmit={name == "" || loadingFolder}
onSubmit={() => { handleCreateFolder() }}
>
<View>

View File

@@ -0,0 +1,65 @@
import Styles from "@/constants/Styles";
import { apiCreateFolderDocument } from "@/lib/api";
import { useAuthSession } from "@/providers/AuthProvider";
import { useLocalSearchParams } from "expo-router";
import { useState } from "react";
import { Pressable, View } from "react-native";
import Toast from "react-native-toast-message";
import Text from "../Text";
import { InputForm } from "../inputForm";
import ModalFloat from "../modalFloat";
export function ModalNewFolder({ path, onCreated }: { path: string, onCreated: () => void }) {
const { token, decryptToken } = useAuthSession()
const [newFolder, setNewFolder] = useState(false)
const [name, setName] = useState("")
const [loadingFolder, setLoadingFolder] = useState(false)
const { id } = useLocalSearchParams<{ id: string }>();
async function handleCreateFolder() {
try {
setLoadingFolder(true)
const hasil = await decryptToken(String(token?.current))
const response = await apiCreateFolderDocument({ data: { user: hasil, name, path, idDivision: id } })
if (response.success) {
Toast.show({ type: 'small', text1: 'Berhasil membuat folder baru', })
} else {
Toast.show({ type: 'small', text1: response.message, })
}
} catch (error) {
console.error(error)
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
} finally {
onCreated()
setLoadingFolder(false)
setNewFolder(false)
}
}
return (
<>
<Pressable style={[Styles.pv05, Styles.borderRight, { width: '50%' }]} onPress={() => setNewFolder(true)}>
<Text style={[Styles.textDefaultSemiBold, { textAlign: 'center' }]}>FOLDER BARU</Text>
</Pressable>
<ModalFloat
title="Buat Folder Baru"
isVisible={newFolder}
setVisible={() => { setNewFolder(false) }}
disableSubmit={name == "" || loadingFolder}
onSubmit={() => { handleCreateFolder() }}
>
<View>
<InputForm
type="default"
placeholder="Nama Folder"
required
label="Nama Folder"
onChange={(value: string) => { setName(value) }}
/>
</View>
</ModalFloat>
</>
)
}

View File

@@ -8,6 +8,7 @@ import { Pressable, View } from "react-native"
import BorderBottomItem from "../borderBottomItem"
import DrawerBottom from "../drawerBottom"
import Text from "../Text"
import { ModalNewFolder } from "./modalNewFolder"
type Props = {
open: boolean
@@ -106,9 +107,7 @@ export default function ModalSalinMove({ open, close, category, onConfirm, dataC
}
</View>
<View style={[Styles.rowOnly, Styles.mt15, Styles.absolute0]}>
<Pressable style={[Styles.pv05, Styles.borderRight, { width: '50%' }]} onPress={() => close(false)}>
<Text style={[Styles.textDefaultSemiBold, { textAlign: 'center' }]}>BATAL</Text>
</Pressable>
<ModalNewFolder path={path} onCreated={() => getData()} />
<Pressable style={[Styles.pv05, { width: '50%' }]} onPress={() => onConfirm(path)}>
<Text style={[Styles.textDefaultSemiBold, { textAlign: 'center' }]}>{category == 'copy' ? 'SALIN' : 'PINDAH'}</Text>
</Pressable>

View File

@@ -11,7 +11,7 @@ type Props = {
export default function ImageWithLabel({ src, label, onClick }: Props) {
return (
<TouchableOpacity style={[Styles.contentItemCenter, Styles.mh05, { width: 70 }]} onPress={onClick}>
<TouchableOpacity style={[Styles.contentItemCenter, Styles.mh03, { width: 55 }]} onPress={onClick}>
<ImageUser src={src} border />
<Text numberOfLines={1} ellipsizeMode="tail" style={[{ textAlign: 'center' }]}>{label}</Text>
</TouchableOpacity>

View File

@@ -28,7 +28,7 @@ type Props = {
export function InputDate({ label, value, placeholder, onChange, info, disable, error, errorText, required, mode, round, width, }: Props) {
const [modal, setModal] = useState(false);
const [valueFix, setValueFix] = useState(new Date())
const [valueFirst, setValueFirst] = useState("")
const [valueFirst, setValueFirst] = useState(mode == "date" ? dayjs(new Date()).format("DD-MM-YYYY") : mode == "time" ? dayjs(new Date()).format("HH:mm") : "")
const onChangeDate = (type: string, selectedDate: any) => {
if (type === "set") {
@@ -45,6 +45,8 @@ export function InputDate({ label, value, placeholder, onChange, info, disable,
onChange(formatted)
setModal(false)
}
} else if (type === "dismissed") {
setModal(false)
}
};
@@ -100,11 +102,8 @@ export function InputDate({ label, value, placeholder, onChange, info, disable,
value={valueFix}
mode={mode}
display="spinner"
onChange={(event, date) => {
onChangeDate(event.type, date)
}}
onChange={(event, date) => { onChangeDate(event.type, date) }}
onTouchCancel={() => setModal(false)}
/>
</ModalFloat>
)
@@ -115,7 +114,7 @@ export function InputDate({ label, value, placeholder, onChange, info, disable,
mode={mode}
display="inline"
onChange={(event, date) => { onChangeDate(event.type, date) }}
onTouchCancel={() => setModal(false)}
onTouchCancel={() => { setModal(false) }}
/>
)
)

View File

@@ -1,93 +1,19 @@
import Styles from "@/constants/Styles"
import { apiCreatePosition } from "@/lib/api"
import { setUpdatePosition } from "@/lib/positionSlice"
import { useAuthSession } from "@/providers/AuthProvider"
import { AntDesign } from "@expo/vector-icons"
import { useEffect, useState } from "react"
import { useState } from "react"
import { View } from "react-native"
import Toast from "react-native-toast-message"
import { useDispatch, useSelector } from "react-redux"
import { ButtonForm } from "../buttonForm"
import { useSelector } from "react-redux"
import ButtonMenuHeader from "../buttonMenuHeader"
import DrawerBottom from "../drawerBottom"
import { InputForm } from "../inputForm"
import MenuItemRow from "../menuItemRow"
import ModalFilter from "../modalFilter"
import ModalSelect from "../modalSelect"
import SelectForm from "../selectForm"
import ModalFormCreatePosition from "./modalFormCreatePosition"
export default function HeaderRightPositionList() {
const dispatch = useDispatch()
const update = useSelector((state: any) => state.positionUpdate)
const { token, decryptToken } = useAuthSession()
const entityUser = useSelector((state: any) => state.user)
const [isVisible, setVisible] = useState(false)
const [isVisibleTambah, setVisibleTambah] = useState(false)
const [isFilter, setFilter] = useState(false)
const [isSelect, setSelect] = useState(false)
const [choose, setChoose] = useState({ val: '', label: '' })
const [disable, setDisable] = useState(true)
const [dataForm, setDataForm] = useState({
name: "",
idGroup: "",
})
const [error, setError] = useState({
name: false,
idGroup: false
});
function validationForm(val: any, cat: 'name' | 'idGroup') {
if (cat === 'name') {
setDataForm({ ...dataForm, name: val })
if (val == "") {
setError({ ...error, name: true })
} else {
setError({ ...error, name: false })
}
} else if (cat === "idGroup") {
setDataForm({ ...dataForm, idGroup: val })
if (val == "") {
setError({ ...error, idGroup: true })
} else {
setError({ ...error, idGroup: false })
}
}
}
function checkAll() {
let nilai = false
if (dataForm.name == "") {
nilai = true
}
if ((entityUser.role == "supadmin" || entityUser.role == "developer") && (dataForm.idGroup == "" || String(dataForm.idGroup) == "null")) {
nilai = true
}
setDisable(nilai)
}
useEffect(() => {
checkAll()
}, [dataForm])
async function handleTambah() {
try {
setDisable(true)
const hasil = await decryptToken(String(token?.current))
const response = await apiCreatePosition({ user: hasil, name: dataForm.name, idGroup: dataForm.idGroup })
dispatch(setUpdatePosition(!update))
} catch (error) {
console.error(error)
} finally {
setDisable(false)
setVisibleTambah(false)
setVisible(false)
Toast.show({ type: 'small', text1: 'Berhasil menambahkan data', })
}
}
return (
<>
@@ -121,64 +47,13 @@ export default function HeaderRightPositionList() {
</DrawerBottom>
<DrawerBottom animation="slide" height={45} keyboard isVisible={isVisibleTambah} setVisible={() => setVisibleTambah(false)} title="Tambah Jabatan">
<View style={{ flex: 1, justifyContent: 'space-between' }}>
<View>
{
(entityUser.role == 'supadmin' || entityUser.role == 'developer') &&
<SelectForm
label="Lembaga Desa"
placeholder="Pilih Lembaga Desa"
value={choose.label}
required
onPress={() => {
setVisibleTambah(false)
setTimeout(() => {
setSelect(true)
}, 600)
}}
error={error.idGroup}
errorText="Lembaga Desa harus diisi"
/>
}
<InputForm
type="default"
placeholder="Nama Jabatan"
required
label="Jabatan"
onChange={(value) => { validationForm(value, 'name') }}
error={error.name}
errorText="Nama jabatan harus diisi"
value={dataForm.name}
/>
</View>
<View style={Styles.mb30}>
<ButtonForm
text="SIMPAN"
onPress={() => { handleTambah() }}
disabled={disable} />
</View>
</View>
<ModalFormCreatePosition onClose={() => setVisibleTambah(false)} />
</DrawerBottom>
<ModalFilter close={() => {
setFilter(false)
setVisible(false)
}} open={isFilter} page="position" />
<ModalSelect
category="group"
close={setSelect}
onSelect={(value) => {
validationForm(value.val, 'idGroup')
setChoose(value)
setSelect(false)
setTimeout(() => {
setVisibleTambah(true)
}, 600)
}}
title="Lembaga Desa"
open={isSelect}
/>
</>
)
}

View File

@@ -0,0 +1,133 @@
import Styles from "@/constants/Styles"
import { apiCreatePosition } from "@/lib/api"
import { setUpdatePosition } from "@/lib/positionSlice"
import { useAuthSession } from "@/providers/AuthProvider"
import { update } from "@react-native-firebase/database"
import { useEffect, useState } from "react"
import { View } from "react-native"
import Toast from "react-native-toast-message"
import { useDispatch, useSelector } from "react-redux"
import { ButtonForm } from "../buttonForm"
import { InputForm } from "../inputForm"
import SelectForm from "../selectForm"
import ModalSelect from "../modalSelect"
export default function ModalFormCreatePosition({ onClose }: { onClose: () => void }) {
const dispatch = useDispatch()
const { token, decryptToken } = useAuthSession()
const entityUser = useSelector((state: any) => state.user)
const [choose, setChoose] = useState({ val: '', label: '' })
const [isSelect, setSelect] = useState(false)
const [disable, setDisable] = useState(true)
const [dataForm, setDataForm] = useState({
name: "",
idGroup: "",
})
const [error, setError] = useState({
name: false,
idGroup: false
});
function validationForm(val: any, cat: 'name' | 'idGroup') {
if (cat === 'name') {
setDataForm({ ...dataForm, name: val })
if (val == "") {
setError({ ...error, name: true })
} else {
setError({ ...error, name: false })
}
} else if (cat === "idGroup") {
setDataForm({ ...dataForm, idGroup: val })
if (val == "") {
setError({ ...error, idGroup: true })
} else {
setError({ ...error, idGroup: false })
}
}
}
function checkAll() {
let nilai = false
if (dataForm.name == "") {
nilai = true
}
if ((entityUser.role == "supadmin" || entityUser.role == "developer") && (dataForm.idGroup == "" || String(dataForm.idGroup) == "null")) {
nilai = true
}
setDisable(nilai)
}
useEffect(() => {
checkAll()
}, [dataForm])
async function handleTambah() {
try {
setDisable(true)
const hasil = await decryptToken(String(token?.current))
const response = await apiCreatePosition({ user: hasil, name: dataForm.name, idGroup: dataForm.idGroup })
dispatch(setUpdatePosition(!update))
} catch (error) {
console.error(error)
} finally {
setDisable(false)
Toast.show({ type: 'small', text1: 'Berhasil menambahkan data', })
onClose()
}
}
return (
<>
<View style={{ flex: 1, justifyContent: 'space-between' }}>
<View>
{
(entityUser.role == 'supadmin' || entityUser.role == 'developer') &&
<SelectForm
label="Lembaga Desa"
placeholder="Pilih Lembaga Desa"
value={choose.label}
required
onPress={() => {
setSelect(true)
}}
error={error.idGroup}
errorText="Lembaga Desa harus diisi"
/>
}
<InputForm
type="default"
placeholder="Nama Jabatan"
required
label="Jabatan"
onChange={(value) => { validationForm(value, 'name') }}
error={error.name}
errorText="Nama jabatan harus diisi"
value={dataForm.name}
/>
</View>
<View style={Styles.mb30}>
<ButtonForm
text="SIMPAN"
onPress={() => { handleTambah() }}
disabled={disable} />
</View>
</View>
<ModalSelect
category="group"
close={setSelect}
onSelect={(value) => {
validationForm(value.val, 'idGroup')
setChoose(value)
setSelect(false)
}}
title="Lembaga Desa"
open={isSelect}
valChoose={choose.val}
/>
</>
)
}

View File

@@ -44,6 +44,9 @@ const Styles = StyleSheet.create({
fontSize: 20,
fontWeight: 'bold',
},
textSubtitle2: {
fontSize: 20,
},
textLink: {
fontSize: 14,
color: '#0a7ea4',
@@ -94,6 +97,9 @@ const Styles = StyleSheet.create({
mv15: {
marginVertical: 15
},
mh03: {
marginHorizontal: 3
},
mh05: {
marginHorizontal: 5
},

View File

@@ -1,24 +1,56 @@
{
"cli": {
"version": ">= 16.10.0",
"appVersionSource": "remote"
"appVersionSource": "local"
},
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
"distribution": "internal",
"android": {
"buildType": "apk"
},
"ios": {
"simulator": true
}
},
"preview": {
"distribution": "internal",
"android": {
"buildType": "apk"
},
"ios": {
"simulator": false
}
},
"production": {
"autoIncrement": true
"distribution": "store",
"android": {
"buildType": "app-bundle"
},
"ios": {
"simulator": false
}
}
},
"submit": {
"production": {}
"production": {
"android": {
"serviceAccountKeyPath": "./service-account.json",
"track": "production"
}
},
"beta": {
"android": {
"serviceAccountKeyPath": "./service-account.json",
"track": "beta",
"releaseStatus": "completed"
}
},
"internal": {
"android": {
"serviceAccountKeyPath": "./service-account.json",
"track": "internal"
}
}
}
}
}

File diff suppressed because one or more lines are too long

View File

@@ -15,9 +15,9 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
BuildableName = "mobiledarmasaba.app"
BlueprintName = "mobiledarmasaba"
ReferencedContainer = "container:mobiledarmasaba.xcodeproj">
BuildableName = "Desa.app"
BlueprintName = "Desa"
ReferencedContainer = "container:Desa.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
@@ -33,9 +33,9 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "00E356ED1AD99517003FC87E"
BuildableName = "mobiledarmasabaTests.xctest"
BlueprintName = "mobiledarmasabaTests"
ReferencedContainer = "container:mobiledarmasaba.xcodeproj">
BuildableName = "DesaTests.xctest"
BlueprintName = "DesaTests"
ReferencedContainer = "container:Desa.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
@@ -55,9 +55,9 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
BuildableName = "mobiledarmasaba.app"
BlueprintName = "mobiledarmasaba"
ReferencedContainer = "container:mobiledarmasaba.xcodeproj">
BuildableName = "Desa.app"
BlueprintName = "Desa"
ReferencedContainer = "container:Desa.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
@@ -72,9 +72,9 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
BuildableName = "mobiledarmasaba.app"
BlueprintName = "mobiledarmasaba"
ReferencedContainer = "container:mobiledarmasaba.xcodeproj">
BuildableName = "Desa.app"
BlueprintName = "Desa"
ReferencedContainer = "container:Desa.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>

View File

@@ -2,7 +2,7 @@
<Workspace
version = "1.0">
<FileRef
location = "group:mobiledarmasaba.xcodeproj">
location = "group:Desa.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">

View File

@@ -1,5 +1,5 @@
<?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/>
</plist>

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -7,7 +7,7 @@
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>mobile-darmasaba</string>
<string>Desa+</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
@@ -19,7 +19,7 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<string>1.0.2</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
@@ -53,6 +53,14 @@
<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>

View File

@@ -13,7 +13,7 @@ install! 'cocoapods',
prepare_react_native_project!
target 'mobiledarmasaba' do
target 'Desa' do
use_expo_modules!
if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1'
@@ -27,12 +27,11 @@ target 'mobiledarmasaba' do
'--platform',
'ios'
]
pod 'FirebaseDatabase', :modular_headers => true
pod 'FirebaseCore', :modular_headers => true
pod 'FirebaseAppCheckInterop', :modular_headers => true
pod 'leveldb-library', :modular_headers => true
pod 'GoogleUtilities', :modular_headers => true
pod 'FirebaseAppCheckInterop', :modular_headers => true
pod 'leveldb-library', :modular_headers => true
pod 'GoogleUtilities', :modular_headers => true
end
config = use_native_modules!(config_command)

View File

@@ -2027,6 +2027,8 @@ PODS:
- React-utils (= 0.79.5)
- RNCAsyncStorage (2.1.2):
- React-Core
- RNCClipboard (1.16.3):
- React-Core
- RNDateTimePicker (8.4.1):
- React-Core
- RNFBApp (22.4.0):
@@ -2328,6 +2330,7 @@ DEPENDENCIES:
- ReactCodegen (from `build/generated/ios`)
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
- "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)"
- "RNCClipboard (from `../node_modules/@react-native-clipboard/clipboard`)"
- "RNDateTimePicker (from `../node_modules/@react-native-community/datetimepicker`)"
- "RNFBApp (from `../node_modules/@react-native-firebase/app`)"
- "RNFBDatabase (from `../node_modules/@react-native-firebase/database`)"
@@ -2570,6 +2573,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon"
RNCAsyncStorage:
:path: "../node_modules/@react-native-async-storage/async-storage"
RNCClipboard:
:path: "../node_modules/@react-native-clipboard/clipboard"
RNDateTimePicker:
:path: "../node_modules/@react-native-community/datetimepicker"
RNFBApp:
@@ -2712,6 +2717,7 @@ SPEC CHECKSUMS:
ReactCodegen: 272c9bc1a8a917bf557bd9d032a4b3e181c6abfe
ReactCommon: 7eb76fcd5133313d8c6a138a5c7dd89f80f189d5
RNCAsyncStorage: b9f5f78da5d16a853fe3dc22e8268d932fc45a83
RNCClipboard: f6679d470d0da2bce2a37b0af7b9e0bf369ecda5
RNDateTimePicker: 60f9e986d61e42169a2716c1b51f1f93dfa82665
RNFBApp: 12884d3bf9b3a0223efe4a0adce516edf72c4102
RNFBDatabase: 1e5c4bda4bb47a48820089ddef498f9af21cb52b
@@ -2724,6 +2730,6 @@ SPEC CHECKSUMS:
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
Yoga: adb397651e1c00672c12e9495babca70777e411e
PODFILE CHECKSUM: c5fe5ebbeae04f6245009795a1dcd457ba36c8e6
PODFILE CHECKSUM: 7f3e2b6dc54bdba333a1261a914e9d69e33f503f
COCOAPODS: 1.16.2

Binary file not shown.

Before

Width:  |  Height:  |  Size: 713 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 359 KiB

View File

@@ -2,9 +2,6 @@ import axios from 'axios';
import Constants from 'expo-constants';
const api = axios.create({
// baseURL: 'http://10.0.2.2:3000/api',
// baseURL: 'https://stg-darmasaba.wibudev.com/api',
// baseURL: 'http://192.168.154.198:3000/api',
baseURL: Constants?.expoConfig?.extra?.URL_API
});
@@ -14,8 +11,7 @@ export const apiCheckPhoneLogin = async (body: { phone: string }) => {
}
export const apiSendOtp = async (body: { phone: string, otp: number }) => {
const res = await axios.get(`${Constants.expoConfig?.extra?.URL_OTP}/code?nom=${body.phone}&text=*DARMASABA*%0A%0A
JANGAN BERIKAN KODE RAHASIA ini kepada siapa pun TERMASUK PIHAK DARMASABA. Masukkan otentikasi: *${encodeURIComponent(body.otp)}*`)
const res = await axios.get(`${Constants.expoConfig?.extra?.URL_OTP}/code?nom=${body.phone}&text=*Desa%2B*%0AMasukkan%20kode%20ini%20*${encodeURIComponent(body.otp)}*%20pada%20aplikasi%20Desa%2B%20anda.%20Jangan%20berikan%20pada%20siapapun.`)
return res.status
}

View File

@@ -41,6 +41,7 @@ export const requestPermission = async () => {
}
return false
}
return true
} else if (Platform.OS === 'ios') {
const { status } = await Notifications.requestPermissionsAsync();
return status === 'granted';

View File

@@ -9,7 +9,9 @@
"ios": "expo run:ios",
"web": "expo start --web",
"test": "jest --watchAll",
"lint": "expo lint"
"lint": "expo lint",
"bump": "node bump-version.js",
"build:android": "npm run bump && eas build -p android --profile production"
},
"jest": {
"preset": "jest-expo"
@@ -19,6 +21,7 @@
"@expo/vector-icons": "^14.0.2",
"@formatjs/intl-getcanonicallocales": "^2.5.5",
"@react-native-async-storage/async-storage": "2.1.2",
"@react-native-clipboard/clipboard": "^1.16.3",
"@react-native-community/cli": "^19.1.0",
"@react-native-community/datetimepicker": "8.4.1",
"@react-native-firebase/app": "^22.4.0",

View File

@@ -61,6 +61,8 @@ export default function AuthProvider({ children }: { children: ReactNode }): Rea
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: "" })
}
} catch (error) {
console.error(error)
@@ -84,6 +86,8 @@ export default function AuthProvider({ children }: { children: ReactNode }): Rea
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)