Compare commits

...

24 Commits

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

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

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

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

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

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

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

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

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

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

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

No Issues
2025-10-07 11:02:47 +08:00
9fa19af68b Merge pull request 'amalia/06-okt-25' (#46) from amalia/06-okt-25 into join
Reviewed-on: bip/mobile-darmasaba#46
2025-10-06 17:25:13 +08:00
c4e48726e0 Merge pull request 'amalia/03-okt-25' (#45) from amalia/03-okt-25 into join
Reviewed-on: bip/mobile-darmasaba#45
2025-10-03 17:31:32 +08:00
24 changed files with 557 additions and 132 deletions

View File

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

View File

@@ -19,7 +19,6 @@ import { getMessaging, onMessage } from "@react-native-firebase/messaging";
import { Redirect, router, Stack, usePathname } from "expo-router";
import { StatusBar } from 'expo-status-bar';
import { useEffect } from "react";
import { Platform } from "react-native";
import { Easing, Notifier } from 'react-native-notifier';
import { Provider } from "react-redux";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -74,7 +74,7 @@ export default function CreateDivisionAddAdmin() {
return (
<SafeAreaView>
<>
<Stack.Screen
options={{
headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
@@ -91,7 +91,7 @@ export default function CreateDivisionAddAdmin() {
)
}}
/>
<View style={[Styles.p15]}>
<View style={[Styles.p15, { flex: 1 }]}>
<ScrollView>
{
data.length > 0 ?
@@ -126,6 +126,6 @@ export default function CreateDivisionAddAdmin() {
}
</ScrollView>
</View>
</SafeAreaView>
</>
)
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import { ColorsStatus } from "@/constants/ColorsStatus";
import Styles from "@/constants/Styles";
import React from "react";
import React, { useState } from "react";
import { Dimensions, Pressable, View } from "react-native";
import Text from "./Text";
@@ -11,6 +11,7 @@ type Props = {
desc?: string
rightTopInfo?: string
onPress?: () => void
onLongPress?: () => void
borderType: 'all' | 'bottom' | 'none'
leftBottomInfo?: React.ReactNode | string
rightBottomInfo?: React.ReactNode | string
@@ -18,16 +19,32 @@ type Props = {
bgColor?: 'white' | 'transparent'
width?: number
descEllipsize?: boolean
textColor?: string
textColor?: string,
colorPress?: boolean
}
export default function BorderBottomItem({ title, subtitle, icon, desc, onPress, rightTopInfo, borderType, leftBottomInfo, rightBottomInfo, titleWeight, bgColor, width, descEllipsize, textColor }: Props) {
export default function BorderBottomItem({ title, subtitle, icon, desc, onPress, onLongPress, rightTopInfo, borderType, leftBottomInfo, rightBottomInfo, titleWeight, bgColor, width, descEllipsize, textColor, colorPress }: Props) {
const lebarDim = Dimensions.get("window").width;
const lebar = width ? lebarDim * width / 100 : 'auto';
const textColorFix = textColor ? textColor : 'black';
const [isTap, setIsTap] = useState(false);
return (
<Pressable style={[borderType == 'bottom' ? Styles.wrapItemBorderBottom : borderType == 'all' ? Styles.wrapItemBorderAll : Styles.wrapItemBorderNone, bgColor && bgColor == 'white' && ColorsStatus.white]} onPress={onPress}>
<Pressable onLongPress={onLongPress} onPress={onPress}
onPressIn={() => setIsTap(true)}
onPressOut={() => setIsTap(false)}
style={({ pressed }) => [
borderType == 'bottom'
? Styles.wrapItemBorderBottom
: borderType == 'all'
? Styles.wrapItemBorderAll
: Styles.wrapItemBorderNone,
bgColor && bgColor == 'white' && ColorsStatus.white,
// efek warna saat ditekan (sementara)
isTap && colorPress && ColorsStatus.pressedGray,
]}
>
<View style={[Styles.rowItemsCenter]}>
{icon}
<View style={[Styles.rowSpaceBetween, width ? { width: lebar } : { width: '88%' }]}>
@@ -52,16 +69,16 @@ export default function BorderBottomItem({ title, subtitle, icon, desc, onPress,
{
(leftBottomInfo || rightBottomInfo) &&
(
<View style={[Styles.rowSpaceBetween, Styles.mt10]}>
<View style={[rightBottomInfo && !leftBottomInfo ? Styles.rowSpaceBetweenReverse : Styles.rowSpaceBetween, Styles.mt05]}>
{
typeof leftBottomInfo == 'string' ?
<Text style={[Styles.textInformation, Styles.cGray, { textAlign: 'left' }]}>{leftBottomInfo}</Text>
<Text style={[Styles.textInformation, Styles.cGray]}>{leftBottomInfo}</Text>
:
leftBottomInfo
}
{
typeof rightBottomInfo == 'string' ?
<Text style={[Styles.textInformation, Styles.mt05, Styles.cGray, { textAlign: 'right' }]}>{rightBottomInfo}</Text>
<Text style={[Styles.textInformation, Styles.cGray]}>{rightBottomInfo}</Text>
:
rightBottomInfo
}

View File

@@ -20,10 +20,11 @@ type Props = {
disable?: boolean
multiline?: boolean
mb?: boolean
focus?: boolean
};
export function InputForm({ label, value, placeholder, onChange, info, disable, error, errorText, required, itemLeft, itemRight, type, round, width, bg, multiline, mb = true }: Props) {
export function InputForm({ label, value, placeholder, onChange, info, disable, error, errorText, required, itemLeft, itemRight, type, round, width, bg, multiline, mb = true, focus }: Props) {
const lebar = Dimensions.get("window").width;
if (itemLeft != undefined || itemRight != undefined) {

View File

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

View File

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

View File

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

View File

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

View File

@@ -187,6 +187,9 @@ const Styles = StyleSheet.create({
pv15: {
paddingVertical: 15
},
pv20: {
paddingVertical: 20
},
p15: {
padding: 15
},
@@ -247,6 +250,10 @@ const Styles = StyleSheet.create({
justifyContent: 'space-between',
flexDirection: 'row'
},
rowSpaceBetweenReverse: {
justifyContent: 'space-between',
flexDirection: 'row-reverse'
},
rowItemsCenter: {
flexDirection: 'row',
alignItems: 'center'

View File

@@ -19,7 +19,7 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>2.0.1</string>
<string>2.0.4</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
@@ -39,7 +39,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>3</string>
<string>6</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSMinimumSystemVersion</key>

View File

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