Compare commits

...

33 Commits

Author SHA1 Message Date
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
e9f1b14bd6 Merge pull request 'amalia/28-agustus-25' (#31) from amalia/28-agustus-25 into join
Reviewed-on: bip/mobile-darmasaba#31
2025-08-28 17:41:58 +08:00
9607774056 fix : create division
Deskripsi:
- fix error form create division

No Issues
2025-08-28 16:39:03 +08:00
07caea8ae5 fix : tampilan
Deskripsi:
- tag text warna hitam pada info division page

No Issues
2025-08-28 15:47:10 +08:00
94c48889c6 upd: validasi create pengumuman
Deskripsi :.
- disable button form pada saat blm memilih divisi

No Issues
2025-08-28 14:49:56 +08:00
d0849143f2 upd: tampilan
Deskripsi:
- menghilangkan new line dan tag html pada list pengumuman dan list diskusi umum
- update api

NO Issues
2025-08-28 14:29:31 +08:00
a7aeb3d3f9 upd
: validasi no telp

Deskripsi:
- validasi nomor telepon >= 9 dan <=16
- tambah anggota, edit anggota, edit profile

No Issues
2025-08-28 12:00:46 +08:00
e755273ab1 Merge pull request 'amalia/27-agustus-25' (#30) from amalia/27-agustus-25 into join
Reviewed-on: bip/mobile-darmasaba#30
2025-08-27 17:34:22 +08:00
d460653408 fix: jabatan
Deskripsi:
- loading button on form edit dan tambah data
- toast error api edit jabatan

No Issues
2025-08-27 17:06:43 +08:00
bb242c9be8 upd: tampilan android
Deskripsi:
- padding input pada tampilan android

No Issues
2025-08-27 15:50:32 +08:00
c01a1885c2 fix : toast
Deskripsi:
- bisa custom posisi toast alert

No Issues
2025-08-27 11:39:46 +08:00
2651e4bd18 upd : position
Deskripsi:
- validasi button form tambah jabatan
- validasi disable button form edit jabatan

No Issues
2025-08-27 11:25:16 +08:00
171c5f0eeb upd: ketika link api berubah
Deskripsi:
- jika pengembalian error maka otomatis signout pada halaman home

NO Issues
2025-08-27 10:30:23 +08:00
abeb26e565 Merge pull request 'fix : tampilan' (#29) from amalia/26-agustus-25 into join
Reviewed-on: bip/mobile-darmasaba#29
2025-08-26 17:23:42 +08:00
191699af04 fix : tampilan
Deskripsi:
- text panjang pada pengumuman
- text panjang pada list banner
- text align pada detail member
- text panjang pada section item tanggal tugas
- text panjang pada select form
- text panjang pada detail event calendar divisi
- keyboard avoiding pada edit event calendar divisi

No Issues
2025-08-26 16:35:25 +08:00
5cb81856de Merge pull request 'amalia/22-agustus-25' (#28) from amalia/22-agustus-25 into join
Reviewed-on: bip/mobile-darmasaba#28
2025-08-22 17:42:40 +08:00
37fda41dc1 upd: tampilan
Deskripsi:
- jarak setelah input text pencarian

No Issues
2025-08-22 16:40:31 +08:00
298113488c upd: task divisi
Deskripsi:
- update role akses task divisi

No Issues'
2025-08-22 16:29:52 +08:00
d88c332b03 upd: tampilan
Deskripsi:
- vertical center icon list dan grid pada list project dan divisi
- mb list lembaga desa dan search page

No Issues
2025-08-22 12:15:45 +08:00
f5c29e86fa upd : banner
Deskripsi:
- home > ketika tidak ada data banner
- banner list > ketika tidak ada data banner

No Issues
2025-08-22 12:00:23 +08:00
61 changed files with 768 additions and 529 deletions

2
.gitignore vendored
View File

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

View File

@@ -4,7 +4,7 @@ export default {
expo: {
name: "mobile-darmasaba",
slug: "mobile-darmasaba",
version: "1.0.0",
version: "1.0.2",
jsEngine: "jsc",
orientation: "portrait",
icon: "./assets/images/icon.png",
@@ -21,11 +21,19 @@ export default {
},
android: {
package: "mobiledarmasaba.app",
versionCode: 6,
adaptiveIcon: {
foregroundImage: "./assets/images/splash-icon.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",

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

@@ -84,9 +84,9 @@ export default function DetailAnnouncement() {
</View>
:
<>
<View style={Styles.rowItemsCenter}>
<View style={[Styles.rowItemsCenter, { alignItems: 'flex-start' }]}>
<MaterialIcons name="campaign" size={30} color="black" style={Styles.mr05} />
<Text style={[Styles.textDefaultSemiBold]}>{data?.title}</Text>
<Text style={[Styles.textDefaultSemiBold, Styles.w90]}>{data?.title}</Text>
</View>
<View style={[Styles.mt10]}>
{

View File

@@ -99,7 +99,7 @@ export default function CreateAnnouncement() {
headerTitleAlign: "center",
headerRight: () => (
<ButtonSaveHeader
disable={disableBtn || loading ? true : false}
disable={disableBtn || divisionMember.length == 0 || loading ? true : false}
category="create"
onPress={() => {
divisionMember.length == 0

View File

@@ -87,7 +87,7 @@ export default function Announcement() {
<View>
<InputSearch onChange={setSearch} />
</View>
<View style={[{ flex: 2 }]}>
<View style={[{ flex: 2 }, Styles.mt05]}>
{
loading ?
arrSkeleton.map((item, index) => {
@@ -114,7 +114,7 @@ export default function Announcement() {
</View>
}
title={item.title}
desc={item.desc.replace(/<[^>]*>?/gm, '')}
desc={item.desc.replace(/<[^>]*>?/gm, '').replace(/\r?\n|\r/g, ' ')}
rightTopInfo={item.createdAt}
/>
)

View File

@@ -5,6 +5,7 @@ import ButtonBackHeader from "@/components/buttonBackHeader"
import DrawerBottom from "@/components/drawerBottom"
import MenuItemRow from "@/components/menuItemRow"
import ModalLoading from "@/components/modalLoading"
import Text from "@/components/Text"
import { ConstEnv } from "@/constants/ConstEnv"
import Styles from "@/constants/Styles"
import { apiDeleteBanner, apiGetBanner } from "@/lib/api"
@@ -121,26 +122,37 @@ export default function BannerList() {
}
style={[Styles.h100]}
>
<View style={[Styles.p15, Styles.mb100]}>
{entities.map((index: any, key: number) => (
<BorderBottomItem
key={key}
onPress={() => {
setDataId(index.id)
setSelectFile(index)
setModal(true)
}}
borderType="all"
icon={
<Image
source={{ uri: `${ConstEnv.url_storage}/files/${index.image}` }}
style={[Styles.imgListBanner]}
{
entities.length > 0
?
<View style={[Styles.p15, Styles.mb100]}>
{entities.map((index: any, key: number) => (
<BorderBottomItem
key={key}
onPress={() => {
setDataId(index.id)
setSelectFile(index)
setModal(true)
}}
borderType="all"
icon={
<Image
source={{ uri: `${ConstEnv.url_storage}/files/${index.image}` }}
style={[Styles.imgListBanner]}
/>
}
title={index.title}
width={65}
/>
}
title={index.title}
/>
))}
</View>
))}
</View>
:
<View style={[Styles.p15, Styles.mb100]}>
<Text style={[Styles.textDefault, Styles.cGray, { textAlign: 'center' }]}>Tidak ada data</Text>
</View>
}
</ScrollView>
<DrawerBottom animation="slide" isVisible={isModal} setVisible={() => setModal(false)} title="Menu">

View File

@@ -122,7 +122,7 @@ export default function Discussion() {
</View>
}
</View>
<View style={[{ flex: 2 }]}>
<View style={[{ flex: 2 }, Styles.mt05]}>
{
loading ?
arrSkeleton.map((item: any, i: number) => {
@@ -153,7 +153,7 @@ export default function Discussion() {
status != "false" && <LabelStatus category={item.status === 1 ? "success" : "error"} text={item.status === 1 ? "BUKA" : "TUTUP"} size="small" />
}
rightTopInfo={item.createdAt}
desc={item.desc}
desc={item.desc.replace(/<[^>]*>?/gm, ' ').replace(/\r?\n|\r/g, ' ')}
leftBottomInfo={
<View style={[Styles.rowItemsCenter]}>
<Ionicons name="chatbox-ellipses-outline" size={18} color="grey" style={Styles.mr05} />
@@ -176,34 +176,6 @@ export default function Discussion() {
/>
}
/>
// data.map((item: any, i: number) => {
// return (
// <BorderBottomItem
// key={i}
// onPress={() => { router.push(`/discussion/${item.id}`) }}
// borderType="bottom"
// icon={
// <View style={[Styles.iconContent, ColorsStatus.lightGreen]}>
// <MaterialIcons name="chat" size={25} color={'#384288'} />
// </View>
// }
// title={item.title}
// subtitle={
// status != "false" && <LabelStatus category={item.status === 1 ? "success" : "error"} text={item.status === 1 ? "BUKA" : "TUTUP"} size="small" />
// }
// rightTopInfo={item.createdAt}
// desc={item.desc}
// leftBottomInfo={
// <View style={[Styles.rowItemsCenter]}>
// <Ionicons name="chatbox-ellipses-outline" size={18} color="grey" style={Styles.mr05} />
// <Text style={[Styles.textInformation, Styles.cGray, Styles.mb05]}>Diskusikan</Text>
// </View>
// }
// rightBottomInfo={`${item.total_komentar} Komentar`}
// />
// )
// })
:
<Text style={[Styles.textDefault, Styles.cGray, { textAlign: 'center' }]}>Tidak ada data</Text>
}

View File

@@ -9,10 +9,11 @@ import { valueTypeEventRepeat } from "@/constants/TypeEventRepeat"
import { apiGetCalendarOne, apiUpdateCalendar } from "@/lib/api"
import { stringToDateTime } from "@/lib/fun_stringToDate"
import { useAuthSession } from "@/providers/AuthProvider"
import { useHeaderHeight } from "@react-navigation/elements"
import { Stack, router, useLocalSearchParams } from "expo-router"
import moment from "moment"
import { useEffect, useState } from "react"
import { SafeAreaView, ScrollView, View } from "react-native"
import { KeyboardAvoidingView, Platform, SafeAreaView, ScrollView, View } from "react-native"
import Toast from "react-native-toast-message"
export default function EditEventCalendar() {
@@ -22,6 +23,7 @@ export default function EditEventCalendar() {
const { id, detail } = useLocalSearchParams<{ id: string, detail: string }>()
const [idCalendar, setIdCalendar] = useState('')
const [loading, setLoading] = useState(false)
const headerHeight = useHeaderHeight()
const [error, setError] = useState({
title: false,
@@ -176,94 +178,100 @@ export default function EditEventCalendar() {
/>
}}
/>
<ScrollView>
<View style={[Styles.p15, Styles.mb100]}>
<InputForm
label="Nama Acara"
type="default"
placeholder="Nama Acara"
required
bg="white"
value={data.title}
onChange={(val) => validationForm("title", val)}
error={error.title}
errorText="Nama acara tidak boleh kosong"
/>
<InputDate
onChange={(val) => validationForm("dateStart", val)}
mode="date"
value={data.dateStart}
label="Tanggal Acara"
required
error={error.dateStart}
errorText="Tanggal acara tidak boleh kosong"
placeholder="Pilih Tanggal Acara"
/>
<View style={[Styles.rowSpaceBetween, Styles.mv10]}>
<View style={[{ width: "48%" }]}>
<InputDate
onChange={(val) => validationForm("timeStart", val)}
mode="time"
value={data.timeStart}
label="Waktu Awal"
required
error={error.timeStart}
errorText="Waktu awal tidak valid"
placeholder="--:--"
/>
</View>
<View style={[{ width: "48%" }]}>
<InputDate
onChange={(val) => validationForm("timeEnd", val)}
mode="time"
value={data.timeEnd}
label="Waktu Akhir"
required
error={error.timeEnd}
errorText="Waktu akhir tidak valid"
placeholder="--:--"
/>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
keyboardVerticalOffset={headerHeight}
>
<ScrollView>
<View style={[Styles.p15]}>
<InputForm
label="Nama Acara"
type="default"
placeholder="Nama Acara"
required
bg="white"
value={data.title}
onChange={(val) => validationForm("title", val)}
error={error.title}
errorText="Nama acara tidak boleh kosong"
/>
<InputDate
onChange={(val) => validationForm("dateStart", val)}
mode="date"
value={data.dateStart}
label="Tanggal Acara"
required
error={error.dateStart}
errorText="Tanggal acara tidak boleh kosong"
placeholder="Pilih Tanggal Acara"
/>
<View style={[Styles.rowSpaceBetween, Styles.mv10]}>
<View style={[{ width: "48%" }]}>
<InputDate
onChange={(val) => validationForm("timeStart", val)}
mode="time"
value={data.timeStart}
label="Waktu Awal"
required
error={error.timeStart}
errorText="Waktu awal tidak valid"
placeholder="--:--"
/>
</View>
<View style={[{ width: "48%" }]}>
<InputDate
onChange={(val) => validationForm("timeEnd", val)}
mode="time"
value={data.timeEnd}
label="Waktu Akhir"
required
error={error.timeEnd}
errorText="Waktu akhir tidak valid"
placeholder="--:--"
/>
</View>
</View>
<InputForm
label="Link Meet"
type="default"
placeholder="Link Meet"
bg="white"
value={data.linkMeet}
onChange={(val) => validationForm("linkMeet", val)}
/>
<SelectForm
bg="white"
label="Ulangi Acara"
placeholder="Ulangi Acara"
value={choose.label}
required
onPress={() => { setSelect(true) }}
/>
<InputForm
label="Jumlah Pengulangan"
type="numeric"
placeholder="Jumlah Pengulangan"
required
bg="white"
value={String(data.repeatValue)}
onChange={(val) => validationForm("repeatValue", val)}
error={error.repeatValue}
errorText="Jumlah pengulangan tidak valid"
disable={choose.val == "once"}
/>
<InputForm
label="Deskripsi"
type="default"
placeholder="Deskripsi"
bg="white"
value={data.desc}
onChange={(val) => validationForm("desc", val)}
multiline
/>
</View>
<InputForm
label="Link Meet"
type="default"
placeholder="Link Meet"
bg="white"
value={data.linkMeet}
onChange={(val) => validationForm("linkMeet", val)}
/>
<SelectForm
bg="white"
label="Ulangi Acara"
placeholder="Ulangi Acara"
value={choose.label}
required
onPress={() => { setSelect(true) }}
/>
<InputForm
label="Jumlah Pengulangan"
type="numeric"
placeholder="Jumlah Pengulangan"
required
bg="white"
value={String(data.repeatValue)}
onChange={(val) => validationForm("repeatValue", val)}
error={error.repeatValue}
errorText="Jumlah pengulangan tidak valid"
disable={choose.val == "once"}
/>
<InputForm
label="Deskripsi"
type="default"
placeholder="Deskripsi"
bg="white"
value={data.desc}
onChange={(val) => validationForm("desc", val)}
multiline
/>
</View>
</ScrollView>
</ScrollView>
</KeyboardAvoidingView>
<ModalSelect
category={"type-event-repeat"}

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));
@@ -165,12 +171,12 @@ export default function DetailEventCalendar() {
>
<View style={[Styles.p15]}>
<View style={[Styles.wrapPaper, Styles.mb15]}>
<View style={Styles.rowItemsCenter}>
<View style={[Styles.rowItemsCenter, { alignItems: 'flex-start' }]}>
<MaterialCommunityIcons name="calendar-text" size={30} color="black" style={Styles.mr10} />
{
loading ?
<Skeleton width={80} height={10} borderRadius={10} widthType="percent" />
: <Text style={[Styles.textDefault]}>{data?.title}</Text>
: <Text style={[Styles.textDefault, Styles.w90]}>{data?.title}</Text>
}
</View>
@@ -216,16 +222,20 @@ 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]}>
<View style={[Styles.rowItemsCenter, Styles.mt10, { alignItems: 'flex-start' }]}>
<MaterialCommunityIcons name="card-text-outline" size={30} color="black" style={Styles.mr10} />
{
loading ?
<Skeleton width={80} height={10} borderRadius={10} widthType="percent" />
:
<Text style={[Styles.textDefault]}>{data?.desc}</Text>
<Text style={[Styles.textDefault, Styles.w90]}>{data?.desc}</Text>
}
</View>
</View>

View File

@@ -121,7 +121,7 @@ export default function DiscussionDivision() {
<InputSearch onChange={setSearch} />
</View>
<View style={[{ flex: 2 }]}>
<View style={[{ flex: 2 }, Styles.mt05]}>
{
loading ?
arrSkeleton.map((item: any, i: number) => {

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

@@ -8,7 +8,7 @@ import SectionMemberTask from "@/components/task/sectionMemberTask";
import SectionReportTask from "@/components/task/sectionReportTask";
import SectionTanggalTugasTask from "@/components/task/sectionTanggalTugasTask";
import Styles from "@/constants/Styles";
import { apiGetTaskOne } from "@/lib/api";
import { apiGetDivisionOneFeature, apiGetTaskOne } from "@/lib/api";
import { useAuthSession } from "@/providers/AuthProvider";
import { router, Stack, useLocalSearchParams } from "expo-router";
import { useEffect, useState } from "react";
@@ -32,6 +32,35 @@ export default function DetailTaskDivision() {
const [progress, setProgress] = useState(0)
const update = useSelector((state: any) => state.taskUpdate)
const [refreshing, setRefreshing] = useState(false)
const [isMemberDivision, setIsMemberDivision] = useState(false);
const [isAdminDivision, setIsAdminDivision] = useState(false);
const entityUser = useSelector((state: any) => state.user);
async function handleCheckMember() {
try {
const hasil = await decryptToken(String(token?.current));
const response = await apiGetDivisionOneFeature({
id,
user: hasil,
cat: "check-member",
});
setIsMemberDivision(response.data);
const response2 = await apiGetDivisionOneFeature({
id,
user: hasil,
cat: "check-admin",
});
setIsAdminDivision(response2.data);
} catch (error) {
console.error(error);
}
}
useEffect(() => {
handleCheckMember()
}, [])
async function handleLoad(cat: 'data' | 'progress') {
@@ -74,7 +103,9 @@ export default function DetailTaskDivision() {
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
headerTitle: loading ? 'Loading... ' : data?.title,
headerTitleAlign: 'center',
headerRight: () => <HeaderRightTaskDetail id={detail} division={id} status={data?.status} />,
headerRight: () => (entityUser.role == "user" || entityUser.role == "coadmin") && !isMemberDivision
? <></>
: <HeaderRightTaskDetail id={detail} division={id} status={data?.status} isAdminDivision={isAdminDivision} />,
}}
/>
<ScrollView
@@ -91,10 +122,10 @@ export default function DetailTaskDivision() {
}
<SectionProgress text={`Kemajuan Kegiatan ${progress}%`} progress={progress} />
<SectionReportTask refreshing={refreshing} />
<SectionTanggalTugasTask refreshing={refreshing} />
<SectionFileTask refreshing={refreshing} />
<SectionLinkTask refreshing={refreshing} />
<SectionMemberTask refreshing={refreshing} />
<SectionTanggalTugasTask refreshing={refreshing} isMemberDivision={isMemberDivision} />
<SectionFileTask refreshing={refreshing} isMemberDivision={isMemberDivision} />
<SectionLinkTask refreshing={refreshing} isMemberDivision={isMemberDivision} />
<SectionMemberTask refreshing={refreshing} isMemberDivision={isMemberDivision} />
</View>
</ScrollView>
</SafeAreaView>

View File

@@ -179,7 +179,7 @@ export default function ListTask() {
</Pressable>
</View>
</View>
<View style={[{ flex: 2 }]}>
<View style={[{ flex: 2 }, Styles.mt05]}>
{
loading ?
isList ?

View File

@@ -7,6 +7,7 @@ import ImageUser from "@/components/imageNew"
import SectionCancel from "@/components/sectionCancel"
import Skeleton from "@/components/skeleton"
import SkeletonTwoItem from "@/components/skeletonTwoItem"
import Text from "@/components/Text"
import { ColorsStatus } from "@/constants/ColorsStatus"
import { ConstEnv } from "@/constants/ConstEnv"
import Styles from "@/constants/Styles"
@@ -15,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, Text, View } from "react-native"
import { Pressable, SafeAreaView, ScrollView, View } from "react-native"
import Toast from "react-native-toast-message"
import { useSelector } from "react-redux"

View File

@@ -51,9 +51,8 @@ export default function CreateDivisionAddAdmin() {
async function handleAddMember() {
try {
setLoading(true)
dispatch(setFormCreateDivision({ ...update, admin: selectMember }))
const hasil = await decryptToken(String(token?.current))
const response = await apiCreateDivision({ ...update, user: hasil })
const response = await apiCreateDivision({ ...update, admin: selectMember, user: hasil })
if (response.success) {
Toast.show({ type: 'small', text1: 'Berhasil membuat divisi', })
dispatch(setFormCreateDivision({ admin: [], member: [], data: { idGroup: '', name: '', desc: '' } }))

View File

@@ -180,7 +180,7 @@ export default function ListDivision() {
</View>
}
<View style={[Styles.rowSpaceBetween]}>
<View style={[Styles.rowSpaceBetween, { alignItems: 'center' }]}>
<InputSearch width={68} onChange={setSearch} />
<Pressable
onPress={() => {
@@ -200,7 +200,7 @@ export default function ListDivision() {
</View>
)}
</View>
<View style={[{ flex: 2 }]}>
<View style={[{ flex: 2 }, Styles.mt05]}>
{
loading ?
isList ?

View File

@@ -122,7 +122,7 @@ export default function EditProfile() {
}
} else if (cat == "phone") {
setData({ ...data, phone: val });
if (val == "" || !(val.length >= 10 && val.length <= 15)) {
if (val == "" || !(val.length >= 9 && val.length <= 16)) {
setError({ ...error, phone: true });
} else {
setError({ ...error, phone: false });

View File

@@ -34,7 +34,7 @@ export default function Index() {
const arrSkeleton = Array.from({ length: 5 }, (_, index) => index)
const [loading, setLoading] = useState(true)
const [status, setStatus] = useState<'true' | 'false'>('true')
const [loadingSubmit, setLoadingSubmit] = useState(false)
const [idChoose, setIdChoose] = useState('')
const [activeChoose, setActiveChoose] = useState(true)
const [titleChoose, setTitleChoose] = useState('')
@@ -49,12 +49,14 @@ export default function Index() {
async function handleEdit() {
try {
setLoadingSubmit(true)
const hasil = await decryptToken(String(token?.current))
const response = await apiEditGroup({ user: hasil, name: titleChoose }, idChoose)
dispatch(setUpdateGroup(!update))
} catch (error) {
console.error(error)
} finally {
setLoadingSubmit(false)
setVisibleEdit(false)
setModal(false)
Toast.show({ type: 'small', text1: 'Berhasil mengupdate data', })
@@ -126,7 +128,7 @@ export default function Index() {
return (
<View style={[Styles.p15, { flex: 1 }]}>
<View>
<View style={[Styles.mb10]}>
<View style={[Styles.wrapBtnTab]}>
<ButtonTab
active={status == "false" ? "false" : "true"}
@@ -145,7 +147,7 @@ export default function Index() {
</View>
<InputSearch onChange={setSearch} />
</View>
<View style={{ flex: 2 }}>
<View style={[{ flex: 2 }, Styles.mt05]}>
{
loading ?
arrSkeleton.map((item, index) => {
@@ -234,7 +236,7 @@ export default function Index() {
onChange={(val) => { validationForm(val, 'title') }} />
</View>
<View>
<ButtonForm text="SIMPAN" disabled={Object.values(error).some((v) => v == true) || titleChoose == ""} onPress={() => { handleEdit() }} />
<ButtonForm text="SIMPAN" disabled={Object.values(error).some((v) => v == true) || titleChoose == "" || loadingSubmit} onPress={() => { handleEdit() }} />
</View>
</View>
</DrawerBottom>

View File

@@ -22,8 +22,8 @@ import { useDispatch, useSelector } from "react-redux";
export default function Home() {
const entities = useSelector((state: any) => state.entities)
const dispatch = useDispatch()
const { token, decryptToken } = useAuthSession()
const insets = useSafeAreaInsets();
const { token, decryptToken, signOut } = useAuthSession()
const insets = useSafeAreaInsets()
useEffect(() => {
handleUserLogin()
@@ -31,7 +31,11 @@ export default function Home() {
async function handleUserLogin() {
const hasil = await decryptToken(String(token?.current))
apiGetProfile({ id: hasil }).then((data) => dispatch(setEntities(data.data)));
apiGetProfile({ id: hasil })
.then((data) => dispatch(setEntities(data.data)))
.catch((error) => {
signOut()
});
}
return (

View File

@@ -95,7 +95,7 @@ export default function MemberDetail() {
:
<>
<ImageUser src={`${ConstEnv.url_storage}/files/${data?.img}`} size="lg" />
<Text style={[Styles.textSubtitle, Styles.cWhite, Styles.mt10]}>{data?.name}</Text>
<Text style={[Styles.textSubtitle, Styles.cWhite, Styles.mt10, { textAlign: 'center' }]}>{data?.name}</Text>
<Text style={[Styles.textMediumNormal, Styles.cWhite]}>{data?.role}</Text>
</>

View File

@@ -117,7 +117,7 @@ export default function CreateMember() {
}
} else if (cat == "phone") {
setDataForm({ ...dataForm, phone: val });
if (val == "" || !(val.length >= 10 && val.length <= 15)) {
if (val == "" || !(val.length >= 9 && val.length <= 16)) {
setError({ ...error, phone: true });
} else {
setError({ ...error, phone: false });

View File

@@ -149,7 +149,7 @@ export default function EditMember() {
}
} else if (cat == "phone") {
setData({ ...data, phone: val });
if (val == "" || !(val.length >= 10 && val.length <= 15)) {
if (val == "" || !(val.length >= 9 && val.length <= 16)) {
setError({ ...error, phone: true });
} else {
setError({ ...error, phone: false });

View File

@@ -129,7 +129,7 @@ export default function Index() {
</View>
}
</View>
<View style={[{ flex: 2 }]}>
<View style={[{ flex: 2 }, Styles.mt05]}>
{
loading ?
arrSkeleton.map((item, index) => {

View File

@@ -40,6 +40,7 @@ export default function Index() {
const [data, setData] = useState<Props[]>([])
const [search, setSearch] = useState('')
const [nameGroup, setNameGroup] = useState('')
const [loadingSubmit, setLoadingSubmit] = useState(false)
const [chooseData, setChooseData] = useState({ name: '', id: '', active: false, idGroup: '' })
const [error, setError] = useState({
name: false,
@@ -94,15 +95,20 @@ export default function Index() {
async function handleEdit() {
try {
setLoadingSubmit(true)
const hasil = await decryptToken(String(token?.current))
const response = await apiEditPosition({ user: hasil, name: chooseData.name, idGroup: chooseData.idGroup }, chooseData.id)
dispatch(setUpdatePosition(!update))
if (response.success) {
dispatch(setUpdatePosition(!update))
} else {
Toast.show({ type: 'small', text1: response.message, })
}
} catch (error) {
console.error(error)
} finally {
setLoadingSubmit(false)
setVisibleEdit(false)
setModal(false)
Toast.show({ type: 'small', text1: 'Berhasil mengupdate data', })
}
}
@@ -165,7 +171,7 @@ export default function Index() {
</View>
}
</View>
<View style={[{ flex: 2 }]}>
<View style={[{ flex: 2 }, Styles.mt05]}>
{
loading ?
arrSkeleton.map((item, index) => {
@@ -251,7 +257,7 @@ export default function Index() {
/>
</View>
<View style={Styles.mb30}>
<ButtonForm text="SIMPAN" onPress={() => { checkForm() }} />
<ButtonForm text="SIMPAN" onPress={() => { handleEdit() }} disabled={Object.values(error).some((v) => v == true) || chooseData.name == "" || loadingSubmit} />
</View>
</View>
</DrawerBottom>

View File

@@ -178,7 +178,7 @@ export default function ListProject() {
n={4}
/>
</ScrollView>
<View style={[Styles.rowSpaceBetween]}>
<View style={[Styles.rowSpaceBetween, { alignItems: 'center' }]}>
<InputSearch width={68} onChange={setSearch} />
<Pressable
onPress={() => {

View File

@@ -75,7 +75,6 @@ export default function Search() {
headerTitleAlign: 'center'
}}
/>
{/* <ScrollView> */}
<View style={[Styles.p15]}>
<InputSearch onChange={handleSearch} />
{
@@ -163,13 +162,12 @@ export default function Search() {
</View>
:
<View style={Styles.contentItemCenter}>
<View style={[Styles.contentItemCenter, Styles.mt10]}>
<Text style={[Styles.textInformation, Styles.cGray]}>Tidak ada data</Text>
</View>
}
</View>
{/* </ScrollView> */}
</SafeAreaView>
</>
)

View File

@@ -1,7 +1,6 @@
import ViewLogin from "@/components/auth/viewLogin";
import ViewVerification from "@/components/auth/viewVerification";
import Text from '@/components/Text';
import ToastCustom from "@/components/toastCustom";
import { requestPermission } from "@/lib/useNotification";
import { useAuthSession } from "@/providers/AuthProvider";
import { Redirect } from "expo-router";

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,23 +20,29 @@ 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) {
// localStorage.setItem('user', response.id)
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, })
} catch (error) {
return Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
return Toast.show({ type: 'small', text1: 'Terjadi kesalahan', position: 'top' })
} finally {
setLoadingLogin(false)
}
@@ -43,6 +50,7 @@ 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 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";
@@ -27,7 +28,7 @@ export default function ViewVerification({ phone, otp }: Props) {
const encrypted = await encryptToken(valueUser);
signIn(encrypted);
} else {
return Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
return Toast.show({ type: 'small', text1: 'Terjadi kesalahan', position: 'top' })
}
}
@@ -35,7 +36,7 @@ export default function ViewVerification({ phone, otp }: Props) {
if (value === otpFix.toString()) {
login()
} else {
return Toast.show({ type: 'small', text1: 'Kode OTP tidak sesuai' });
return Toast.show({ type: 'small', text1: 'Kode OTP tidak sesuai', position: 'top' });
}
}
@@ -56,6 +57,7 @@ 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 File

@@ -48,7 +48,7 @@ export default function BorderBottomItem({ title, subtitle, icon, desc, onPress,
</View>
</View>
{desc && <Text style={[Styles.textDefault, Styles.mt05, { textAlign: 'justify', color: textColorFix }]} numberOfLines={descEllipsize == false ? 0 : 2} ellipsizeMode='tail'>{desc}</Text>}
{desc && <Text style={[Styles.textDefault, Styles.mt05, { textAlign: 'left', color: textColorFix }]} numberOfLines={descEllipsize == false ? 0 : 2} ellipsizeMode='tail'>{desc}</Text>}
{
(leftBottomInfo || rightBottomInfo) &&
(

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

@@ -44,7 +44,7 @@ export default function DrawerBottom({ isVisible, setVisible, title, children, a
>
<View style={[Styles.modalContentNew, { height: tinggiFix }]}>
<View style={[Styles.titleContainerNew]}>
<Text style={Styles.textDefault}>{title}</Text>
<Text numberOfLines={1} ellipsizeMode='tail' style={[Styles.textDefault, Styles.w90]}>{title}</Text>
<Pressable onPress={() => setVisible(false)}>
<MaterialIcons name="close" color="black" size={22} />
</Pressable>
@@ -57,7 +57,7 @@ export default function DrawerBottom({ isVisible, setVisible, title, children, a
:
<View style={[Styles.modalContentNew, { height: tinggiFix }]}>
<View style={[Styles.titleContainerNew]}>
<Text style={Styles.textDefault}>{title}</Text>
<Text numberOfLines={1} ellipsizeMode='tail' style={[Styles.textDefault, Styles.w90]}>{title}</Text>
<Pressable onPress={() => setVisible(false)}>
<MaterialIcons name="close" color="black" size={22} />
</Pressable>

View File

@@ -1,5 +1,5 @@
import Styles from "@/constants/Styles"
import { Pressable, View } from "react-native"
import Styles from "@/constants/Styles";
import { Pressable, View } from "react-native";
import Text from "./Text";
type Props = {
@@ -17,8 +17,8 @@ export default function EventItem({ category, title, user, jamAwal, jamAkhir, on
<View style={[Styles.dividerEvent, { backgroundColor: category == 'orange' ? '#FB804C' : '#535FCA' }]} />
<View>
<Text>{jamAwal} - {jamAkhir}</Text>
<Text style={[Styles.textDefaultSemiBold, Styles.mv05]}>{title}</Text>
<Text>Dibuat oleh : {user}</Text>
<Text numberOfLines={1} ellipsizeMode="tail" style={[Styles.textDefaultSemiBold, Styles.mv05]}>{title}</Text>
<Text numberOfLines={1} ellipsizeMode="tail">Dibuat oleh : {user}</Text>
</View>
</Pressable>
)

View File

@@ -9,6 +9,7 @@ import { Dimensions, Image, View } from "react-native";
import { useSharedValue } from "react-native-reanimated";
import Carousel, { ICarouselInstance } from "react-native-reanimated-carousel";
import { useDispatch, useSelector } from "react-redux";
import Text from "../Text";
export default function CaraouselHome() {
const { decryptToken, token } = useAuthSession()
@@ -21,7 +22,13 @@ export default function CaraouselHome() {
async function handleBannerView() {
const hasil = await decryptToken(String(token?.current))
apiGetBanner({ user: hasil }).then((data) => dispatch(setEntities(data.data)))
apiGetBanner({ user: hasil }).then((data) => {
if (data.data.length > 0) {
dispatch(setEntities(data.data))
} else {
dispatch(setEntities([]))
}
})
}
async function handleUser() {
@@ -40,22 +47,30 @@ export default function CaraouselHome() {
return (
<View style={[Styles.mv15]}>
<Carousel
ref={ref}
width={width}
height={width / 2.5}
data={entities}
loop={true}
autoPlay={true}
autoPlayInterval={5000}
onProgressChange={progress}
renderItem={({ index }) => (
<Image
source={{ uri: `${ConstEnv.url_storage}/files/${entities[index].image}` }}
style={[Styles.caraoselContent]}
{
entities.length > 0 ?
<Carousel
ref={ref}
width={width}
height={width / 2.5}
data={entities}
loop={true}
autoPlay={true}
autoPlayInterval={5000}
onProgressChange={progress}
renderItem={({ index }) => (
<Image
source={{ uri: `${ConstEnv.url_storage}/files/${entities[index].image}` }}
style={[Styles.caraoselContent]}
/>
)}
/>
)}
/>
:
<View style={[Styles.caraoselContent, { height: width / 2.5 }]}>
<Text style={[Styles.textDefault, Styles.cWhite, { textAlign: 'center' }]}>BANNER</Text>
</View>
}
</View>
)
}

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

@@ -19,15 +19,16 @@ type Props = {
value?: string
disable?: boolean
multiline?: boolean
mb?: boolean
};
export function InputForm({ label, value, placeholder, onChange, info, disable, error, errorText, required, itemLeft, itemRight, type, round, width, bg, multiline }: Props) {
export function InputForm({ label, value, placeholder, onChange, info, disable, error, errorText, required, itemLeft, itemRight, type, round, width, bg, multiline, mb = true }: Props) {
const lebar = Dimensions.get("window").width;
if (itemLeft != undefined || itemRight != undefined) {
return (
<View style={{ marginBottom: 10 }}>
<View style={[mb && Styles.mb10]}>
{
label != undefined && (
<Text style={[{ marginBottom: 5, textTransform: "capitalize" }, error && Styles.cError]}>
@@ -42,7 +43,7 @@ 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 },
Platform.OS == 'ios' ? { paddingVertical: 10 } : { paddingVertical: 0 },
]}>
{itemRight != undefined ? itemRight : itemLeft}
<TextInput
@@ -52,7 +53,7 @@ export function InputForm({ label, value, placeholder, onChange, info, disable,
keyboardType={type}
onChangeText={onChange}
placeholderTextColor={'gray'}
style={[Styles.mh05, { width: width ? lebar * width / 100 : lebar * 0.78, color: 'black' }]}
style={[Styles.mh05, { width: width ? lebar * width / 100 : lebar * 0.78, color: 'black' }, Platform.OS == 'ios' ? { paddingVertical: 1 } : { paddingVertical: 5 }]}
/>
</View>
{error && (<Text style={[Styles.textInformation, Styles.cError, Styles.mt05]}>{errorText}</Text>)}
@@ -78,7 +79,7 @@ export function InputForm({ label, value, placeholder, onChange, info, disable,
placeholder={placeholder}
keyboardType={type}
editable={!disable}
style={[Styles.inputRoundForm, error && { borderColor: "red" }, round && Styles.round30, { backgroundColor: bg && bg == 'white' ? 'white' : 'transparent' }, { color: 'black' }, multiline && { height: 150, textAlignVertical: 'top' }]}
style={[Styles.inputRoundForm, Platform.OS == 'ios' ? { paddingVertical: 11 } : { paddingVertical: 6 }, error && { borderColor: "red" }, round && Styles.round30, { backgroundColor: bg && bg == 'white' ? 'white' : 'transparent' }, { color: 'black' }, multiline && { height: 150, textAlignVertical: 'top' }]}
onChangeText={onChange}
placeholderTextColor={'gray'}
multiline={multiline}

View File

@@ -12,6 +12,7 @@ export default function InputSearch({ onChange, width, value }: { onChange?: (va
width={width}
bg="white"
value={value}
mb={false}
/>
)
}

View File

@@ -33,7 +33,7 @@ export default function ItemSectionTanggalTugas({ done, title, dateStart, dateEn
</View>
<View style={[Styles.wrapPaper, Styles.mv10, Styles.p10]}>
<View style={[Styles.rowItemsCenter]}>
<View style={[Styles.rowItemsCenter, {alignItems:'flex-start'}]}>
<MaterialCommunityIcons name="file-table-outline" size={25} color="black" style={[Styles.mr10]} />
<View style={[Styles.w90]}>
<Text style={[Styles.textDefault]}>{title}</Text>

View File

@@ -1,96 +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 { 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 [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 = true
if (dataForm.name == "") {
setError(error => ({ ...error, name: true }))
nilai = false
}
if ((entityUser.role == "supadmin" || entityUser.role == "developer") && (dataForm.idGroup == "" || String(dataForm.idGroup) == "null")) {
setError(error => ({ ...error, idGroup: true }))
nilai = false
}
return nilai
}
function onCheck() {
const check = checkAll()
if (!check)
return false
handleTambah()
}
async function handleTambah() {
try {
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 {
setVisibleTambah(false)
setVisible(false)
Toast.show({ type: 'small', text1: 'Berhasil menambahkan data', })
}
}
return (
<>
@@ -124,61 +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={() => { onCheck() }} />
</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

@@ -34,9 +34,9 @@ export default function SelectForm({ label, value, placeholder, onPress, info, e
<Feather name="chevron-right" size={20} color="grey" />
{
value ? (
<Text style={[Styles.cBlack]}>{value}</Text>
<Text numberOfLines={1} ellipsizeMode='tail' style={[Styles.cBlack, Styles.w90]}>{value}</Text>
) : (
<Text style={[Styles.cGray]}>{placeholder}</Text>
<Text numberOfLines={1} ellipsizeMode='tail' style={[Styles.cGray, Styles.w90]}>{placeholder}</Text>
)
}
</View>

View File

@@ -1,10 +1,10 @@
import Styles from "@/constants/Styles"
import { apiAddLinkTask, apiDeleteTask, apiGetDivisionOneFeature } from "@/lib/api"
import { apiAddLinkTask, apiDeleteTask } from "@/lib/api"
import { setUpdateTask } from "@/lib/taskUpdate"
import { useAuthSession } from "@/providers/AuthProvider"
import { AntDesign, Feather, Ionicons, MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons"
import { router } from "expo-router"
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"
@@ -18,46 +18,19 @@ import ModalFloat from "../modalFloat"
type Props = {
id: string | string[]
division: string
status: number | undefined
status: number | undefined,
isAdminDivision: boolean
}
export default function HeaderRightTaskDetail({ id, division, status }: Props) {
export default function HeaderRightTaskDetail({ id, division, status, isAdminDivision }: Props) {
const { token, decryptToken } = useAuthSession()
const [isVisible, setVisible] = useState(false)
const entityUser = useSelector((state: any) => state.user);
const [isMemberDivision, setIsMemberDivision] = useState(false);
const [isAdminDivision, setIsAdminDivision] = useState(false);
const dispatch = useDispatch()
const update = useSelector((state: any) => state.taskUpdate)
const [isAddLink, setAddLink] = useState(false)
const [link, setLink] = useState("")
async function handleCheckMember() {
try {
const hasil = await decryptToken(String(token?.current));
const response = await apiGetDivisionOneFeature({
id: division,
user: hasil,
cat: "check-member",
});
setIsMemberDivision(response.data);
const response2 = await apiGetDivisionOneFeature({
id: division,
user: hasil,
cat: "check-admin",
});
setIsAdminDivision(response2.data);
} catch (error) {
console.error(error);
}
}
useEffect(() => {
handleCheckMember()
}, [])
async function handleDelete() {
try {
const hasil = await decryptToken(String(token?.current))
@@ -95,12 +68,7 @@ export default function HeaderRightTaskDetail({ id, division, status }: Props) {
return (
<>
{
(entityUser.role == "user" || entityUser.role == "coadmin") && !isMemberDivision
? <></>
:
<ButtonMenuHeader onPress={() => { setVisible(true) }} />
}
<ButtonMenuHeader onPress={() => { setVisible(true) }} />
<DrawerBottom animation="slide" isVisible={isVisible} setVisible={setVisible} title="Menu" height={30}>
<View style={Styles.rowItemsCenter}>
<MenuItemRow

View File

@@ -28,7 +28,7 @@ type Props = {
idStorage: string
}
export default function SectionFileTask({ refreshing }: { refreshing: boolean }) {
export default function SectionFileTask({ refreshing, isMemberDivision }: { refreshing: boolean, isMemberDivision: boolean }) {
const [isModal, setModal] = useState(false)
const { token, decryptToken } = useAuthSession()
const { detail } = useLocalSearchParams<{ detail: string }>()
@@ -39,6 +39,7 @@ export default function SectionFileTask({ refreshing }: { refreshing: boolean })
const arrSkeleton = Array.from({ length: 5 })
const [selectFile, setSelectFile] = useState<Props | null>(null)
const [loadingOpen, setLoadingOpen] = useState(false)
const entityUser = useSelector((state: any) => state.user);
async function handleLoad(loading: boolean) {
try {
@@ -163,21 +164,28 @@ export default function SectionFileTask({ refreshing }: { refreshing: boolean })
openFile()
}}
/>
<MenuItemRow
icon={<Ionicons name="trash" color="black" size={25} />}
title="Hapus"
onPress={() => {
setModal(false)
AlertKonfirmasi({
title: 'Konfirmasi',
desc: 'Apakah Anda yakin ingin menghapus file ini? File yang dihapus tidak dapat dikembalikan',
onPress: () => {
handleDelete()
}
})
{
(entityUser.role != "user" && entityUser.role != "coadmin") || isMemberDivision
?
<MenuItemRow
icon={<Ionicons name="trash" color="black" size={25} />}
title="Hapus"
onPress={() => {
setModal(false)
AlertKonfirmasi({
title: 'Konfirmasi',
desc: 'Apakah Anda yakin ingin menghapus file ini? File yang dihapus tidak dapat dikembalikan',
onPress: () => {
handleDelete()
}
})
}}
/>
:
<></>
}
}}
/>
</View>
</DrawerBottom>
</>

View File

@@ -20,7 +20,7 @@ type Props = {
link: string
}
export default function SectionLinkTask({ refreshing }: { refreshing: boolean }) {
export default function SectionLinkTask({ refreshing, isMemberDivision }: { refreshing: boolean, isMemberDivision: boolean }) {
const [isModal, setModal] = useState(false)
const { token, decryptToken } = useAuthSession()
const { detail } = useLocalSearchParams<{ detail: string }>()
@@ -28,6 +28,7 @@ export default function SectionLinkTask({ refreshing }: { refreshing: boolean })
const update = useSelector((state: any) => state.taskUpdate)
const dispatch = useDispatch()
const [selectLink, setSelectLink] = useState<Props | null>(null)
const entityUser = useSelector((state: any) => state.user);
async function handleLoad() {
try {
@@ -101,18 +102,25 @@ export default function SectionLinkTask({ refreshing }: { refreshing: boolean })
Linking.openURL(urlCompleted(String(selectLink?.link)))
}}
/>
<MenuItemRow
icon={<Ionicons name="trash" color="black" size={25} />}
title="Hapus"
onPress={() => {
setModal(false)
AlertKonfirmasi({
title: 'Konfirmasi',
desc: 'Apakah Anda yakin ingin menghapus link ini? Link yang dihapus tidak dapat dikembalikan',
onPress: () => { handleDelete() }
})
}}
/>
{
(entityUser.role != "user" && entityUser.role != "coadmin") || isMemberDivision
?
<MenuItemRow
icon={<Ionicons name="trash" color="black" size={25} />}
title="Hapus"
onPress={() => {
setModal(false)
AlertKonfirmasi({
title: 'Konfirmasi',
desc: 'Apakah Anda yakin ingin menghapus link ini? Link yang dihapus tidak dapat dikembalikan',
onPress: () => { handleDelete() }
})
}}
/>
:
<></>
}
</View>
</DrawerBottom>
</>

View File

@@ -26,8 +26,9 @@ type Props = {
position: string;
};
export default function SectionMemberTask({ refreshing }: { refreshing: boolean }) {
export default function SectionMemberTask({ refreshing, isMemberDivision }: { refreshing: boolean, isMemberDivision: boolean }) {
const [isModal, setModal] = useState(false);
const entityUser = useSelector((state: any) => state.user);
const { token, decryptToken } = useAuthSession();
const { id, detail } = useLocalSearchParams<{ id: string; detail: string }>();
const [data, setData] = useState<Props[]>([]);
@@ -165,24 +166,31 @@ export default function SectionMemberTask({ refreshing }: { refreshing: boolean
}}
/>
<MenuItemRow
icon={
<MaterialCommunityIcons
name="account-remove"
color="black"
size={25}
{
(entityUser.role != "user" && entityUser.role != "coadmin") || isMemberDivision
?
<MenuItemRow
icon={
<MaterialCommunityIcons
name="account-remove"
color="black"
size={25}
/>
}
title="Keluarkan"
onPress={() => {
setModal(false)
AlertKonfirmasi({
title: "Konfirmasi",
desc: "Apakah Anda yakin ingin mengeluarkan anggota?",
onPress: () => { handleDeleteMember() },
});
}}
/>
}
title="Keluarkan"
onPress={() => {
setModal(false)
AlertKonfirmasi({
title: "Konfirmasi",
desc: "Apakah Anda yakin ingin mengeluarkan anggota?",
onPress: () => { handleDeleteMember() },
});
}}
/>
:
<></>
}
</View>
</DrawerBottom>
</>

View File

@@ -26,8 +26,9 @@ type Props = {
dateEnd: string;
}
export default function SectionTanggalTugasTask({ refreshing }: { refreshing: boolean }) {
export default function SectionTanggalTugasTask({ refreshing, isMemberDivision }: { refreshing: boolean, isMemberDivision: boolean }) {
const dispatch = useDispatch()
const entityUser = useSelector((state: any) => state.user);
const update = useSelector((state: any) => state.taskUpdate)
const [isModal, setModal] = useState(false)
const [isSelect, setSelect] = useState(false)
@@ -155,24 +156,6 @@ export default function SectionTanggalTugasTask({ refreshing }: { refreshing: bo
<DrawerBottom animation="slide" isVisible={isModal} setVisible={setModal} title="Menu">
<View style={Styles.rowItemsCenter}>
<MenuItemRow
icon={<MaterialCommunityIcons name="list-status" color="black" size={25} />}
title="Update Status"
onPress={() => {
setModal(false)
setTimeout(() => {
setSelect(true)
}, 600);
}}
/>
<MenuItemRow
icon={<MaterialCommunityIcons name="pencil-outline" color="black" size={25} />}
title="Edit Tugas"
onPress={() => {
setModal(false)
router.push(`./update/${tugas.id}`)
}}
/>
<MenuItemRow
icon={
<MaterialCommunityIcons
@@ -189,24 +172,57 @@ export default function SectionTanggalTugasTask({ refreshing }: { refreshing: bo
}, 600)
}}
/>
</View>
<View style={[Styles.rowItemsCenter, Styles.mt15]}>
<MenuItemRow
icon={<Ionicons name="trash" color="black" size={25} />}
title="Hapus Tugas"
onPress={() => {
setModal(false)
AlertKonfirmasi({
title: 'Konfirmasi',
desc: 'Apakah anda yakin ingin menghapus data ini?',
onPress: () => {
handleDelete()
}
})
{
(entityUser.role != "user" && entityUser.role != "coadmin") || isMemberDivision
?
<>
<MenuItemRow
icon={<MaterialCommunityIcons name="list-status" color="black" size={25} />}
title="Update Status"
onPress={() => {
setModal(false)
setTimeout(() => {
setSelect(true)
}, 600);
}}
/>
<MenuItemRow
icon={<MaterialCommunityIcons name="pencil-outline" color="black" size={25} />}
title="Edit Tugas"
onPress={() => {
setModal(false)
router.push(`./update/${tugas.id}`)
}}
/>
</>
:
<></>
}
}}
/>
</View>
{
(entityUser.role != "user" && entityUser.role != "coadmin") || isMemberDivision
?
<View style={[Styles.rowItemsCenter, Styles.mt15]}>
<MenuItemRow
icon={<Ionicons name="trash" color="black" size={25} />}
title="Hapus Tugas"
onPress={() => {
setModal(false)
AlertKonfirmasi({
title: 'Konfirmasi',
desc: 'Apakah anda yakin ingin menghapus data ini?',
onPress: () => {
handleDelete()
}
})
}}
/>
</View>
:
<></>
}
</DrawerBottom>
<ModalSelect

View File

@@ -3,9 +3,9 @@ import { View } from "react-native";
import Toast from "react-native-toast-message";
import Text from "./Text";
export default function ToastCustom() {
export default function ToastCustom({ position }: { position?: 'top' | 'bottom' }) {
return (
<Toast autoHide onPress={() => Toast.hide()} visibilityTime={1500} position="bottom" config={{
<Toast autoHide onPress={() => Toast.hide()} visibilityTime={1500} position={position || 'bottom'} config={{
small: ({ text1 }) => (
<View style={[Styles.toastContainer]}>
<Text style={{ fontSize: 12 }}>{text1}</Text>

View File

@@ -94,6 +94,9 @@ const Styles = StyleSheet.create({
mv15: {
marginVertical: 15
},
mh03: {
marginHorizontal: 3
},
mh05: {
marginHorizontal: 5
},
@@ -515,7 +518,7 @@ const Styles = StyleSheet.create({
wrapBtnTab: {
justifyContent: 'space-between',
flexDirection: 'row',
marginBottom: 15,
marginBottom: 10,
borderRadius: 20,
padding: 5,
backgroundColor: 'white',

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"
}
}
}
}

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

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
});
@@ -138,12 +135,8 @@ export const apiDeletePosition = async (data: { user: string, isActive: boolean
};
export const apiEditPosition = async (data: { user: string, name: string, idGroup: string }, id: string) => {
await api.put(`mobile/position/${id}`, data).then(response => {
return response.data;
})
.catch(error => {
console.error('Error:', error);
});
const response = await api.put(`mobile/position/${id}`, data)
return response.data
};
export const apiGetUser = async ({ user, active, search, group, page }: { user: string, active: string, search: string, group?: string, page?: number }) => {

View File

@@ -7,7 +7,9 @@ const divisionCreate = createSlice({
idGroup: "",
name: "",
desc: "",
}, member: [], admin: [],
},
member: [],
admin: [],
},
reducers: {
setFormCreateDivision: (state, action) => {

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)