Compare commits
36 Commits
59459e2c22
...
amalia/09-
| Author | SHA1 | Date | |
|---|---|---|---|
| 4ab8422808 | |||
| 3370f48238 | |||
| 9cd78dae3a | |||
| 209254af23 | |||
| 6c80eb77fb | |||
| ae96a79b69 | |||
| a5c58d0de2 | |||
| c979a68028 | |||
| 2cf5c8d960 | |||
| 9dc4d8dc8d | |||
| 789e4f84f1 | |||
| 99c13b57e1 | |||
| 47ed52e9d2 | |||
| 02904b1e48 | |||
| 8df5b48578 | |||
| 21617f9c4c | |||
| 383ca069d5 | |||
| 267454637f | |||
| f939ddb5f5 | |||
| e135c9dc11 | |||
| 144db584d0 | |||
| 90551399a5 | |||
| 0d46d76c70 | |||
| e3d2752a4c | |||
| 4dcfcbb7a7 | |||
| f27707eb74 | |||
| e4e8b44c45 | |||
| 79c9c9046c | |||
| f6122fb35a | |||
| 2ae88ff2e3 | |||
| fbf25bdd59 | |||
| 600218cb11 | |||
| 18eea92cfd | |||
| 1c3aa308d1 | |||
| c21d928701 | |||
| d8a50cbc75 |
@@ -1,4 +0,0 @@
|
||||
kotlin version: 2.0.21
|
||||
error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output:
|
||||
1. Kotlin compile daemon is ready
|
||||
|
||||
@@ -92,8 +92,8 @@ android {
|
||||
applicationId 'mobiledarmasaba.app'
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 16
|
||||
versionName "2.1.0"
|
||||
versionCode 21
|
||||
versionName "2.2.0"
|
||||
}
|
||||
signingConfigs {
|
||||
debug {
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" tools:node="remove"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" tools:node="remove"/>
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
|
||||
@@ -4,7 +4,7 @@ export default {
|
||||
expo: {
|
||||
name: "Desa+",
|
||||
slug: "mobile-darmasaba",
|
||||
version: "2.1.0", // Versi aplikasi (App Store)
|
||||
version: "2.2.0", // 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: "9",
|
||||
buildNumber: "10",
|
||||
infoPlist: {
|
||||
ITSAppUsesNonExemptEncryption: false,
|
||||
CFBundleDisplayName: "Desa+"
|
||||
@@ -23,7 +23,7 @@ export default {
|
||||
},
|
||||
android: {
|
||||
package: "mobiledarmasaba.app",
|
||||
versionCode: 16,
|
||||
versionCode: 21,
|
||||
adaptiveIcon: {
|
||||
foregroundImage: "./assets/images/logo-icon-small.png",
|
||||
backgroundColor: "#ffffff"
|
||||
@@ -32,9 +32,7 @@ export default {
|
||||
permissions: [
|
||||
"READ_EXTERNAL_STORAGE",
|
||||
"WRITE_EXTERNAL_STORAGE",
|
||||
"READ_MEDIA_IMAGES", // Android 13+
|
||||
"READ_MEDIA_VIDEO", // Android 13+
|
||||
"READ_MEDIA_AUDIO" // Android 13+
|
||||
"READ_MEDIA_AUDIO"
|
||||
]
|
||||
},
|
||||
web: {
|
||||
@@ -56,6 +54,7 @@ export default {
|
||||
"expo-font",
|
||||
"expo-image-picker",
|
||||
"expo-web-browser",
|
||||
"./plugins/withRemoveMediaPermissions",
|
||||
[
|
||||
"@react-native-firebase/app",
|
||||
{
|
||||
|
||||
@@ -1,247 +1,178 @@
|
||||
import AppHeader from "@/components/AppHeader";
|
||||
import BorderBottomItem2 from "@/components/borderBottomItem2";
|
||||
import DiscussionCommentInput from "@/components/discussion_general/discussionCommentInput";
|
||||
import DiscussionCommentList, { CommentFile, CommentItem } from "@/components/discussion_general/discussionCommentList";
|
||||
import HeaderRightDiscussionGeneralDetail from "@/components/discussion_general/headerDiscussionDetail";
|
||||
import DrawerBottom from "@/components/drawerBottom";
|
||||
import ImageUser from "@/components/imageNew";
|
||||
import { InputForm } from "@/components/inputForm";
|
||||
import MenuItemRow from "@/components/menuItemRow";
|
||||
import ModalConfirmation from "@/components/ModalConfirmation";
|
||||
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 { apiDeleteDiscussionGeneralCommentar, apiGetDiscussionGeneralOne, apiSendDiscussionGeneralCommentar, apiUpdateDiscussionGeneralCommentar } from "@/lib/api";
|
||||
import { apiDeleteDiscussionGeneralCommentar, apiGetDiscussionGeneralOne, apiSendDiscussionGeneralCommentar, apiSendDiscussionGeneralCommentarWithFile, apiUpdateDiscussionGeneralCommentar } from "@/lib/api";
|
||||
import { getDB } from "@/lib/firebaseDatabase";
|
||||
import { useAuthSession } from "@/providers/AuthProvider";
|
||||
import { useTheme } from "@/providers/ThemeProvider";
|
||||
import { Feather, Ionicons, MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons";
|
||||
import { Feather, Ionicons, MaterialCommunityIcons } 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 React, { useEffect, useState } from "react";
|
||||
import { KeyboardAvoidingView, Platform, Pressable, RefreshControl, ScrollView, View } from "react-native";
|
||||
import { useEffect, useState } from "react";
|
||||
import { KeyboardAvoidingView, Platform, RefreshControl, ScrollView, View } from "react-native";
|
||||
import Toast from "react-native-toast-message";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
type Props = {
|
||||
id: string
|
||||
isActive: boolean
|
||||
title: string
|
||||
desc: string
|
||||
status: number
|
||||
createdAt: string
|
||||
type DiscussionDetail = {
|
||||
id: string; isActive: boolean; title: string
|
||||
desc: string; status: number; createdAt: string
|
||||
}
|
||||
|
||||
type PropsKomentar = {
|
||||
id: string
|
||||
comment: string
|
||||
createdAt: string
|
||||
idUser: string
|
||||
img: string
|
||||
username: string
|
||||
isEdited: boolean
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
type PropsFile = {
|
||||
id: string;
|
||||
idStorage: string;
|
||||
name: string;
|
||||
extension: string
|
||||
}
|
||||
type PropsFile = { id: string; idStorage: string; name: string; extension: string }
|
||||
|
||||
export default function DetailDiscussionGeneral() {
|
||||
const { token, decryptToken } = useAuthSession()
|
||||
const { colors } = useTheme();
|
||||
const { colors } = useTheme()
|
||||
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[]>([])
|
||||
const { id } = useLocalSearchParams<{ id: string }>()
|
||||
const [data, setData] = useState<DiscussionDetail>()
|
||||
const [dataKomentar, setDataKomentar] = useState<CommentItem[]>([])
|
||||
const [memberDiscussion, setMemberDiscussion] = useState(false)
|
||||
const [fileDiscussion, setFileDiscussion] = useState<PropsFile[]>([])
|
||||
const [komentar, setKomentar] = useState('')
|
||||
const [commentFiles, setCommentFiles] = useState<{ uri: string; name: string }[]>([])
|
||||
const update = useSelector((state: any) => state.discussionGeneralDetailUpdate)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [loadingKomentar, setLoadingKomentar] = useState(true)
|
||||
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 [loadingSend, setLoadingSend] = useState(false)
|
||||
const [isVisible, setVisible] = useState(false)
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
const [selectKomentar, setSelectKomentar] = useState({
|
||||
id: '',
|
||||
comment: ''
|
||||
})
|
||||
const [selectKomentar, setSelectKomentar] = useState<{ id: string; comment: string; files: CommentFile[] }>({ id: '', comment: '', files: [] })
|
||||
const [removedFileIds, setRemovedFileIds] = useState<string[]>([])
|
||||
const [viewEdit, setViewEdit] = useState(false)
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
||||
const reference = ref(getDB(), `/discussion-general/${id}`)
|
||||
const headerHeight = useHeaderHeight()
|
||||
|
||||
useEffect(() => {
|
||||
const onValueChange = reference.on('value', snapshot => {
|
||||
if (snapshot.val() == null) {
|
||||
reference.set({ trigger: true })
|
||||
}
|
||||
if (snapshot.val() == null) reference.set({ trigger: true })
|
||||
handleLoad('komentar', false)
|
||||
});
|
||||
|
||||
// Stop listening for updates when no longer required
|
||||
return () => reference.off('value', onValueChange);
|
||||
}, []);
|
||||
})
|
||||
return () => reference.off('value', onValueChange)
|
||||
}, [])
|
||||
|
||||
function updateTrigger() {
|
||||
reference.once('value', snapshot => {
|
||||
const data = snapshot.val();
|
||||
reference.update({ trigger: !data.trigger });
|
||||
});
|
||||
reference.update({ trigger: !snapshot.val().trigger })
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
async function handleLoad(cat: 'detail' | 'komentar' | 'cek-anggota' | 'file', loading: boolean) {
|
||||
async function handleLoad(cat: 'detail' | 'komentar' | 'cek-anggota' | 'file', showLoading: boolean) {
|
||||
try {
|
||||
if (cat == "detail") {
|
||||
setLoading(loading)
|
||||
} else if (cat == "komentar") {
|
||||
setLoadingKomentar(loading)
|
||||
}
|
||||
|
||||
if (cat === 'detail') setLoading(showLoading)
|
||||
else if (cat === 'komentar') setLoadingKomentar(showLoading)
|
||||
const hasil = await decryptToken(String(token?.current))
|
||||
const response = await apiGetDiscussionGeneralOne({ id: id, user: hasil, cat })
|
||||
|
||||
if (cat == 'detail') {
|
||||
setData(response.data)
|
||||
} else if (cat == 'komentar') {
|
||||
setDataKomentar(response.data)
|
||||
} else if (cat == 'cek-anggota') {
|
||||
setMemberDiscussion(response.data)
|
||||
} else if (cat == 'file') {
|
||||
setFileDiscussion(response.data)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setLoadingKomentar(false)
|
||||
}
|
||||
const response = await apiGetDiscussionGeneralOne({ id, user: hasil, cat })
|
||||
if (cat === 'detail') setData(response.data)
|
||||
else if (cat === 'komentar') setDataKomentar(response.data)
|
||||
else if (cat === 'cek-anggota') setMemberDiscussion(response.data)
|
||||
else if (cat === 'file') setFileDiscussion(response.data)
|
||||
} catch (error) { console.error(error) }
|
||||
finally { setLoading(false); setLoadingKomentar(false) }
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
handleLoad('detail', false)
|
||||
handleLoad('komentar', false)
|
||||
handleLoad('cek-anggota', false)
|
||||
handleLoad('file', false)
|
||||
}, [update]);
|
||||
handleLoad('detail', true); handleLoad('komentar', true)
|
||||
handleLoad('cek-anggota', true); handleLoad('file', true)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
handleLoad('detail', true)
|
||||
handleLoad('komentar', true)
|
||||
handleLoad('cek-anggota', true)
|
||||
handleLoad('file', true)
|
||||
}, []);
|
||||
handleLoad('detail', false); handleLoad('komentar', false)
|
||||
handleLoad('cek-anggota', false); handleLoad('file', false)
|
||||
}, [update])
|
||||
|
||||
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 })
|
||||
}
|
||||
setLoadingSend(true)
|
||||
const hasil = await decryptToken(String(token?.current))
|
||||
let response
|
||||
if (commentFiles.length > 0) {
|
||||
const fd = new FormData()
|
||||
commentFiles.forEach((f, i) =>
|
||||
fd.append(`file${i}`, { uri: f.uri, type: 'application/octet-stream', name: f.name } as any)
|
||||
)
|
||||
fd.append("data", JSON.stringify({ desc: komentar, user: hasil }))
|
||||
response = await apiSendDiscussionGeneralCommentarWithFile(id, fd)
|
||||
} else {
|
||||
response = await apiSendDiscussionGeneralCommentar({ id, data: { desc: komentar, user: hasil } })
|
||||
}
|
||||
if (response.success) {
|
||||
setKomentar(''); setCommentFiles([])
|
||||
updateTrigger()
|
||||
} else {
|
||||
Toast.show({ type: 'small', text1: response.message })
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
const message = error?.response?.data?.message || "Gagal menambahkan data"
|
||||
|
||||
Toast.show({ type: 'small', text1: message })
|
||||
} finally {
|
||||
setLoadingSendKomentar(false)
|
||||
}
|
||||
Toast.show({ type: 'small', text1: error?.response?.data?.message || "Gagal menambahkan data" })
|
||||
} finally { setLoadingSend(false) }
|
||||
}
|
||||
|
||||
async function handleEditKomentar() {
|
||||
try {
|
||||
setLoadingSendKomentar(true)
|
||||
setLoadingSend(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 })
|
||||
}
|
||||
const response = await apiUpdateDiscussionGeneralCommentar({ id: selectKomentar.id, data: { desc: selectKomentar.comment, user: hasil, filesToRemove: removedFileIds } })
|
||||
if (response.success) updateTrigger()
|
||||
else Toast.show({ type: 'small', text1: response.message })
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
const message = error?.response?.data?.message || "Gagal mengupdate data"
|
||||
|
||||
Toast.show({ type: 'small', text1: message })
|
||||
} finally {
|
||||
setLoadingSendKomentar(false)
|
||||
handleViewEditKomentar()
|
||||
}
|
||||
Toast.show({ type: 'small', text1: error?.response?.data?.message || "Gagal mengupdate data" })
|
||||
} finally { setLoadingSend(false); handleViewEditKomentar() }
|
||||
}
|
||||
|
||||
async function handleDeleteKomentar() {
|
||||
try {
|
||||
setLoadingSendKomentar(true)
|
||||
setLoadingSend(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 })
|
||||
}
|
||||
if (response.success) updateTrigger()
|
||||
else Toast.show({ type: 'small', text1: response.message })
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
const message = error?.response?.data?.message || "Gagal menghapus data"
|
||||
|
||||
Toast.show({ type: 'small', text1: message })
|
||||
} finally {
|
||||
setLoadingSendKomentar(false)
|
||||
setVisible(false)
|
||||
}
|
||||
Toast.show({ type: 'small', text1: error?.response?.data?.message || "Gagal menghapus data" })
|
||||
} finally { setLoadingSend(false); setVisible(false) }
|
||||
}
|
||||
|
||||
function handleMenuKomentar(id: string, comment: string) {
|
||||
setSelectKomentar({ id, comment })
|
||||
setVisible(true)
|
||||
}
|
||||
function handleViewEditKomentar() { setVisible(false); setViewEdit(!viewEdit); setRemovedFileIds([]) }
|
||||
|
||||
function handleViewEditKomentar() {
|
||||
setVisible(false)
|
||||
setViewEdit(!viewEdit)
|
||||
const isLocked = data?.status === 2 || !data?.isActive
|
||||
const isMember = memberDiscussion || (entityUser.role !== "user" && entityUser.role !== "coadmin")
|
||||
const canComment = !isLocked && isMember
|
||||
|
||||
function lockedReason() {
|
||||
if (data?.status === 2) return "Diskusi telah ditutup"
|
||||
if (!data?.isActive) return "Diskusi telah diarsipkan"
|
||||
return "Hanya anggota diskusi yang dapat memberikan komentar"
|
||||
}
|
||||
|
||||
const handleRefresh = async () => {
|
||||
setRefreshing(true)
|
||||
handleLoad('detail', false)
|
||||
handleLoad('komentar', false)
|
||||
handleLoad('cek-anggota', false)
|
||||
handleLoad('file', false)
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
handleLoad('detail', false); handleLoad('komentar', false)
|
||||
handleLoad('cek-anggota', false); handleLoad('file', false)
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
setRefreshing(false)
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
||||
headerTitle: 'Diskusi',
|
||||
headerTitleAlign: 'center',
|
||||
// headerRight: () => <HeaderRightDiscussionGeneralDetail id={id} active={data?.isActive !== undefined ? data.isActive : false} status={data?.status !== undefined ? data.status : 0} />,
|
||||
header: () => (
|
||||
<AppHeader
|
||||
title="Diskusi"
|
||||
showBack={true}
|
||||
onPressLeft={() => router.back()}
|
||||
right={<HeaderRightDiscussionGeneralDetail id={id} active={data?.isActive !== undefined ? data.isActive : false} status={data?.status !== undefined ? data.status : 0} />}
|
||||
right={<HeaderRightDiscussionGeneralDetail id={id} active={data?.isActive ?? false} status={data?.status ?? 0} />}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
@@ -250,219 +181,97 @@ export default function DetailDiscussionGeneral() {
|
||||
<ScrollView
|
||||
showsVerticalScrollIndicator={false}
|
||||
style={[Styles.h100, { backgroundColor: colors.background }]}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={() => handleRefresh()}
|
||||
tintColor={colors.icon}
|
||||
/>
|
||||
}
|
||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} tintColor={colors.icon} />}
|
||||
>
|
||||
<View style={[Styles.p15]}>
|
||||
{
|
||||
loading ?
|
||||
<SkeletonContent />
|
||||
:
|
||||
<BorderBottomItem2
|
||||
dataFile={fileDiscussion}
|
||||
descEllipsize={false}
|
||||
borderType="all"
|
||||
bgColor="white"
|
||||
icon={
|
||||
<View style={[Styles.discussionIconCircleLg, { backgroundColor: colors.icon + '20' }]}>
|
||||
<Feather name="message-circle" size={22} color={colors.icon} />
|
||||
</View>
|
||||
}
|
||||
title={data?.title}
|
||||
titleShowAll={true}
|
||||
subtitle={
|
||||
<View style={[Styles.discussionStatusPill, {
|
||||
borderColor: !data?.isActive
|
||||
? '#F59E0B'
|
||||
: data?.status == 1 ? '#10B981' : colors.dimmed + '80',
|
||||
}]}>
|
||||
<Text style={[Styles.discussionStatusText, {
|
||||
color: !data?.isActive
|
||||
? '#F59E0B'
|
||||
: data?.status == 1 ? '#10B981' : colors.dimmed,
|
||||
}]}>
|
||||
{!data?.isActive ? 'Arsip' : data?.status == 1 ? 'Buka' : 'Tutup'}
|
||||
</Text>
|
||||
</View>
|
||||
}
|
||||
desc={data?.desc}
|
||||
leftBottomInfo={
|
||||
<View style={[Styles.rowItemsCenter]}>
|
||||
<Feather name="message-square" size={14} color={colors.dimmed} style={Styles.mr05} />
|
||||
<Text style={[Styles.textInformation, { color: colors.dimmed }]}>{dataKomentar.length} Komentar</Text>
|
||||
</View>
|
||||
}
|
||||
rightBottomInfo={
|
||||
<Text style={[Styles.textInformation, { color: colors.dimmed }]}>{data?.createdAt}</Text>
|
||||
}
|
||||
/>
|
||||
}
|
||||
<View style={[Styles.mt10]}>
|
||||
{
|
||||
loadingKomentar ?
|
||||
arrSkeleton.map((item: any, i: number) => {
|
||||
return (
|
||||
<Skeleton key={i} width={100} widthType="percent" height={40} borderRadius={5} />
|
||||
)
|
||||
})
|
||||
:
|
||||
dataKomentar.map((item, i) => (
|
||||
<Pressable
|
||||
key={i}
|
||||
onPress={() => {
|
||||
setDetailMore((prev: any) =>
|
||||
prev.includes(item.id)
|
||||
? prev.filter((id: string) => id !== item.id)
|
||||
: [...prev, item.id]
|
||||
)
|
||||
}}
|
||||
onLongPress={() => {
|
||||
item.idUser == entities.id && data?.status != 2 && data?.isActive && handleMenuKomentar(item.id, item.comment)
|
||||
}}
|
||||
style={({ pressed }) => [
|
||||
Styles.discussionCommentCard,
|
||||
{
|
||||
backgroundColor: pressed ? colors.icon + '10' : colors.card,
|
||||
borderColor: colors.icon + '20',
|
||||
}
|
||||
]}
|
||||
>
|
||||
<View style={Styles.flex1}>
|
||||
{/* Name + time */}
|
||||
<View style={[Styles.rowSpaceBetween, Styles.itemsCenter, Styles.mb05]}>
|
||||
<View style={[Styles.rowItemsCenter, { gap: 8, flex: 1, marginRight: 8 }]}>
|
||||
<ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} size="xs" />
|
||||
<Text style={[Styles.textMediumSemiBold, { color: colors.text }]} numberOfLines={1}>
|
||||
{item.username}
|
||||
</Text>
|
||||
{item.isEdited && (
|
||||
<Text style={[Styles.discussionEditedText, { color: colors.dimmed }]}>
|
||||
diedit
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<Text style={[Styles.discussionDateText, { color: colors.dimmed, flexShrink: 0 }]}>
|
||||
{item.createdAt}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Comment text */}
|
||||
<Text
|
||||
style={[Styles.textDefault, { color: colors.text }]}
|
||||
numberOfLines={detailMore.includes(item.id) ? 0 : 3}
|
||||
>
|
||||
{item.comment}
|
||||
</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
))
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||
keyboardVerticalOffset={headerHeight}
|
||||
>
|
||||
<View style={[
|
||||
Styles.contentItemCenter,
|
||||
Styles.w100,
|
||||
{ backgroundColor: colors.background },
|
||||
viewEdit && Styles.borderTop
|
||||
]}>
|
||||
{
|
||||
viewEdit ?
|
||||
<>
|
||||
<View style={[Styles.w90, Styles.rowSpaceBetween, Styles.pv05]}>
|
||||
<View style={[Styles.rowItemsCenter]}>
|
||||
<Feather name="edit-3" color={colors.text} size={22} style={[Styles.mh05]} />
|
||||
<Text style={[Styles.textMediumSemiBold]}>Edit Komentar</Text>
|
||||
</View>
|
||||
<Pressable onPress={() => handleViewEditKomentar()}>
|
||||
<MaterialIcons name="close" color={colors.text} size={22} />
|
||||
</Pressable>
|
||||
<View style={Styles.p15}>
|
||||
{loading ? <SkeletonContent /> : (
|
||||
<BorderBottomItem2
|
||||
dataFile={fileDiscussion}
|
||||
descEllipsize={false}
|
||||
borderType="all"
|
||||
bgColor="white"
|
||||
icon={
|
||||
<View style={[Styles.discussionIconCircleLg, { backgroundColor: colors.icon + '20' }]}>
|
||||
<Feather name="message-circle" size={22} color={colors.icon} />
|
||||
</View>
|
||||
<InputForm
|
||||
disable={(data?.status === 2 || !data?.isActive || (!memberDiscussion && (entityUser.role == "user" || entityUser.role == "coadmin")))}
|
||||
type="default"
|
||||
round
|
||||
placeholder="Kirim Komentar"
|
||||
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"))) ? { color: colors.dimmed } : { color: colors.tint }} />
|
||||
</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"
|
||||
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"))) ? { color: colors.dimmed } : { color: colors.tint }} />
|
||||
</Pressable>
|
||||
}
|
||||
/>
|
||||
:
|
||||
<View style={[Styles.pv20, Styles.itemsCenter]}>
|
||||
<Text style={[Styles.textInformation, { color: colors.dimmed }]}>
|
||||
{
|
||||
data?.status == 2 ? "Diskusi telah ditutup" : data?.isActive == false ? "Diskusi telah diarsipkan" : "Hanya anggota diskusi yang dapat memberikan komentar"
|
||||
}
|
||||
}
|
||||
title={data?.title}
|
||||
titleShowAll={true}
|
||||
subtitle={
|
||||
<View style={[Styles.discussionStatusPill, {
|
||||
borderColor: !data?.isActive ? '#F59E0B' : data?.status === 1 ? '#10B981' : colors.dimmed + '80'
|
||||
}]}>
|
||||
<Text style={[Styles.discussionStatusText, {
|
||||
color: !data?.isActive ? '#F59E0B' : data?.status === 1 ? '#10B981' : colors.dimmed
|
||||
}]}>
|
||||
{!data?.isActive ? 'Arsip' : data?.status === 1 ? 'Buka' : 'Tutup'}
|
||||
</Text>
|
||||
</View>
|
||||
}
|
||||
|
||||
}
|
||||
desc={data?.desc}
|
||||
leftBottomInfo={
|
||||
<View style={Styles.rowItemsCenter}>
|
||||
<Feather name="message-square" size={14} color={colors.dimmed} style={Styles.mr05} />
|
||||
<Text style={[Styles.textInformation, { color: colors.dimmed }]}>{dataKomentar.length} Komentar</Text>
|
||||
</View>
|
||||
}
|
||||
rightBottomInfo={<Text style={[Styles.textInformation, { color: colors.dimmed }]}>{data?.createdAt}</Text>}
|
||||
/>
|
||||
)}
|
||||
<DiscussionCommentList
|
||||
data={dataKomentar}
|
||||
loading={loadingKomentar}
|
||||
myId={entities.id}
|
||||
canInteract={data?.status !== 2 && data?.isActive === true}
|
||||
onLongPress={(commentId, comment, files) => {
|
||||
setSelectKomentar({ id: commentId, comment, files })
|
||||
setRemovedFileIds([])
|
||||
setVisible(true)
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : undefined} keyboardVerticalOffset={headerHeight}>
|
||||
{canComment ? (
|
||||
<DiscussionCommentInput
|
||||
mode={viewEdit ? 'edit' : 'new'}
|
||||
value={viewEdit ? selectKomentar.comment : komentar}
|
||||
onChange={viewEdit
|
||||
? (val) => setSelectKomentar({ ...selectKomentar, comment: val })
|
||||
: setKomentar
|
||||
}
|
||||
loading={loadingSend}
|
||||
onSend={viewEdit ? handleEditKomentar : handleKomentar}
|
||||
onCancelEdit={handleViewEditKomentar}
|
||||
files={commentFiles}
|
||||
onAddFile={(newFiles) => setCommentFiles(prev => [...prev, ...newFiles])}
|
||||
onRemoveFile={(idx) => setCommentFiles(prev => prev.filter((_, i) => i !== idx))}
|
||||
existingFiles={viewEdit ? selectKomentar.files.filter(f => !removedFileIds.includes(f.id)) : []}
|
||||
onRemoveExistingFile={(fileId) => setRemovedFileIds(prev => [...prev, fileId])}
|
||||
canSend={canComment}
|
||||
/>
|
||||
) : (
|
||||
<DiscussionCommentInput
|
||||
mode="locked"
|
||||
lockedReason={lockedReason()}
|
||||
value="" onChange={() => {}} loading={false} onSend={() => {}} canSend={false}
|
||||
/>
|
||||
)}
|
||||
</KeyboardAvoidingView>
|
||||
</View >
|
||||
</View>
|
||||
|
||||
<DrawerBottom animation="slide" isVisible={isVisible} setVisible={setVisible} title="Komentar">
|
||||
<View style={Styles.rowItemsCenter}>
|
||||
<MenuItemRow
|
||||
icon={<MaterialCommunityIcons name="pencil-outline" color={colors.text} size={25} />}
|
||||
title="Edit"
|
||||
onPress={() => { handleViewEditKomentar() }}
|
||||
onPress={() => handleViewEditKomentar()}
|
||||
/>
|
||||
<MenuItemRow
|
||||
icon={<Ionicons name="trash-outline" color={colors.text} size={25} />}
|
||||
title="Hapus"
|
||||
onPress={() => {
|
||||
setVisible(false)
|
||||
setTimeout(() => {
|
||||
setShowDeleteModal(true)
|
||||
}, 600)
|
||||
}}
|
||||
onPress={() => { setVisible(false); setTimeout(() => setShowDeleteModal(true), 600) }}
|
||||
/>
|
||||
</View>
|
||||
</DrawerBottom>
|
||||
@@ -471,14 +280,11 @@ export default function DetailDiscussionGeneral() {
|
||||
visible={showDeleteModal}
|
||||
title="Konfirmasi"
|
||||
message="Apakah anda yakin ingin menghapus komentar?"
|
||||
onConfirm={() => {
|
||||
setShowDeleteModal(false)
|
||||
handleDeleteKomentar()
|
||||
}}
|
||||
onConfirm={() => { setShowDeleteModal(false); handleDeleteKomentar() }}
|
||||
onCancel={() => setShowDeleteModal(false)}
|
||||
confirmText="Hapus"
|
||||
cancelText="Batal"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,69 +6,159 @@ import InputSearch from "@/components/inputSearch";
|
||||
import Text from '@/components/Text';
|
||||
import { ConstEnv } from "@/constants/ConstEnv";
|
||||
import Styles from "@/constants/Styles";
|
||||
import { apiAddMemberDiscussionGeneral, apiGetDiscussionGeneralOne, apiGetUser } from "@/lib/api";
|
||||
import { apiAddMemberDiscussionGeneral, apiGetDiscussionGeneralOne, apiGetDivision, apiGetDivisionMember, apiGetUser } from "@/lib/api";
|
||||
import { setUpdateDiscussionGeneralDetail } from "@/lib/discussionGeneralDetail";
|
||||
import { useAuthSession } from "@/providers/AuthProvider";
|
||||
import { useTheme } from "@/providers/ThemeProvider";
|
||||
import { AntDesign } from "@expo/vector-icons";
|
||||
import { AntDesign, Ionicons } 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 { ActivityIndicator, Pressable, ScrollView, View } from "react-native";
|
||||
import Toast from "react-native-toast-message";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
|
||||
type Props = {
|
||||
idUser: string,
|
||||
name: string,
|
||||
img: string
|
||||
type Member = { idUser: string; name: string; img: string }
|
||||
|
||||
type DivisionItem = {
|
||||
id: string
|
||||
name: string
|
||||
expanded: boolean
|
||||
membersLoaded: boolean
|
||||
members: Member[]
|
||||
}
|
||||
|
||||
export default function AddMemberDiscussionDetail() {
|
||||
const dispatch = useDispatch()
|
||||
const update = useSelector((state: any) => state.discussionGeneralDetailUpdate)
|
||||
const { token, decryptToken } = useAuthSession()
|
||||
const { colors } = useTheme();
|
||||
const { colors } = useTheme()
|
||||
const { id } = useLocalSearchParams<{ id: string }>()
|
||||
const [dataOld, setDataOld] = useState<Props[]>([])
|
||||
const [data, setData] = useState<Props[]>([])
|
||||
const [dataOld, setDataOld] = useState<any[]>([])
|
||||
const [idGroup, setIdGroup] = useState('')
|
||||
const [selectMember, setSelectMember] = useState<any[]>([])
|
||||
const [selectMember, setSelectMember] = useState<Member[]>([])
|
||||
const [search, setSearch] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [divisions, setDivisions] = useState<DivisionItem[]>([])
|
||||
const [loadingDivisions, setLoadingDivisions] = useState(false)
|
||||
const [loadingIds, setLoadingIds] = useState<string[]>([])
|
||||
const [searchResults, setSearchResults] = useState<Member[]>([])
|
||||
const [loadingSearch, setLoadingSearch] = useState(false)
|
||||
|
||||
async function handleLoad() {
|
||||
try {
|
||||
const hasil = await decryptToken(String(token?.current))
|
||||
const response = await apiGetDiscussionGeneralOne({ id: id, user: hasil, cat: 'anggota' })
|
||||
setDataOld(response.data)
|
||||
const responseGroup = await apiGetDiscussionGeneralOne({ id: id, user: hasil, cat: 'detail' })
|
||||
setIdGroup(responseGroup.data.idGroup)
|
||||
const [resAnggota, resDetail] = await Promise.all([
|
||||
apiGetDiscussionGeneralOne({ id, user: hasil, cat: 'anggota' }),
|
||||
apiGetDiscussionGeneralOne({ id, user: hasil, cat: 'detail' })
|
||||
])
|
||||
setDataOld(resAnggota.data ?? [])
|
||||
setIdGroup(resDetail.data.idGroup)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLoadMember() {
|
||||
const hasil = await decryptToken(String(token?.current))
|
||||
const response = await apiGetUser({ user: hasil, active: "true", search: search, group: String(idGroup) })
|
||||
setData(response.data.filter((i: any) => i.idUserRole != 'supadmin'))
|
||||
async function loadDivisions(group: string) {
|
||||
if (!group) return
|
||||
setLoadingDivisions(true)
|
||||
try {
|
||||
const hasil = await decryptToken(String(token?.current))
|
||||
const response = await apiGetDivision({ user: hasil, search: '', group, active: 'true', kategori: 'semua', page: 1 })
|
||||
const divisionList: DivisionItem[] = (response.data ?? []).map((d: any) => ({
|
||||
id: d.id, name: d.name, expanded: false, membersLoaded: false, members: []
|
||||
}))
|
||||
const withMembers = await Promise.all(
|
||||
divisionList.map(async (d) => {
|
||||
try {
|
||||
const res = await apiGetDivisionMember({ user: hasil, id: d.id, search: '' })
|
||||
const members: Member[] = (res.data ?? []).map((m: any) => ({ idUser: m.idUser, name: m.name, img: m.img }))
|
||||
return { ...d, members, membersLoaded: true }
|
||||
} catch {
|
||||
return { ...d, membersLoaded: true }
|
||||
}
|
||||
})
|
||||
)
|
||||
setDivisions(withMembers)
|
||||
} catch { setDivisions([]) }
|
||||
finally { setLoadingDivisions(false) }
|
||||
}
|
||||
|
||||
async function fetchMembers(divisionId: string): Promise<Member[]> {
|
||||
setLoadingIds(prev => [...prev, divisionId])
|
||||
try {
|
||||
const hasil = await decryptToken(String(token?.current))
|
||||
const response = await apiGetDivisionMember({ user: hasil, id: divisionId, search: '' })
|
||||
const members: Member[] = (response.data ?? []).map((m: any) => ({ idUser: m.idUser, name: m.name, img: m.img }))
|
||||
setDivisions(prev => prev.map(d =>
|
||||
d.id === divisionId ? { ...d, members, membersLoaded: true } : d
|
||||
))
|
||||
return members
|
||||
} catch { return [] }
|
||||
finally { setLoadingIds(prev => prev.filter(i => i !== divisionId)) }
|
||||
}
|
||||
|
||||
async function searchUsers(query: string) {
|
||||
if (!idGroup) return
|
||||
setLoadingSearch(true)
|
||||
try {
|
||||
const hasil = await decryptToken(String(token?.current))
|
||||
const response = await apiGetUser({ user: hasil, active: 'true', search: query, group: idGroup })
|
||||
setSearchResults((response.data ?? [])
|
||||
.filter((i: any) => i.idUserRole !== 'supadmin')
|
||||
.map((i: any) => ({ idUser: i.id, name: i.name, img: i.img }))
|
||||
)
|
||||
} catch { setSearchResults([]) }
|
||||
finally { setLoadingSearch(false) }
|
||||
}
|
||||
|
||||
useEffect(() => { handleLoad() }, [])
|
||||
|
||||
useEffect(() => {
|
||||
handleLoad()
|
||||
}, []);
|
||||
|
||||
if (idGroup) loadDivisions(idGroup)
|
||||
}, [idGroup])
|
||||
|
||||
useEffect(() => {
|
||||
handleLoadMember()
|
||||
if (!idGroup) return
|
||||
if (search) {
|
||||
searchUsers(search)
|
||||
} else {
|
||||
setSearchResults([])
|
||||
loadDivisions(idGroup)
|
||||
}
|
||||
}, [search])
|
||||
|
||||
function onChoose(val: string, label: string, img?: string) {
|
||||
if (selectMember.some((i: any) => i.idUser == val)) {
|
||||
setSelectMember(selectMember.filter((i: any) => i.idUser != val))
|
||||
async function handleTapDivision(division: DivisionItem) {
|
||||
let members = division.members
|
||||
if (!division.membersLoaded) members = await fetchMembers(division.id)
|
||||
setDivisions(prev => prev.map(d =>
|
||||
d.id === division.id ? { ...d, expanded: true, members, membersLoaded: true } : d
|
||||
))
|
||||
const eligible = members.filter(m => !dataOld.some((o: any) => o.idUser === m.idUser))
|
||||
const allSelected = eligible.length > 0 && eligible.every(m =>
|
||||
selectMember.some(s => s.idUser === m.idUser)
|
||||
)
|
||||
if (allSelected) {
|
||||
setSelectMember(prev => prev.filter(s => !eligible.some(m => m.idUser === s.idUser)))
|
||||
} else {
|
||||
setSelectMember([...selectMember, { idUser: val, name: label, img }])
|
||||
const existingIds = new Set(selectMember.map(s => s.idUser))
|
||||
setSelectMember(prev => [...prev, ...eligible.filter(m => !existingIds.has(m.idUser))])
|
||||
}
|
||||
}
|
||||
|
||||
async function handleToggleExpand(divisionId: string) {
|
||||
const division = divisions.find(d => d.id === divisionId)!
|
||||
if (!division.membersLoaded && !division.expanded) await fetchMembers(divisionId)
|
||||
setDivisions(prev => prev.map(d =>
|
||||
d.id === divisionId ? { ...d, expanded: !d.expanded } : d
|
||||
))
|
||||
}
|
||||
|
||||
function handleToggleMember(member: Member) {
|
||||
if (dataOld.some((o: any) => o.idUser === member.idUser)) return
|
||||
if (selectMember.some(s => s.idUser === member.idUser)) {
|
||||
setSelectMember(prev => prev.filter(s => s.idUser !== member.idUser))
|
||||
} else {
|
||||
setSelectMember(prev => [...prev, member])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,41 +166,26 @@ export default function AddMemberDiscussionDetail() {
|
||||
try {
|
||||
setLoading(true)
|
||||
const hasil = await decryptToken(String(token?.current))
|
||||
const response = await apiAddMemberDiscussionGeneral({ id: id, data: { user: hasil, member: selectMember } })
|
||||
const response = await apiAddMemberDiscussionGeneral({ id, data: { user: hasil, member: selectMember } })
|
||||
if (response.success) {
|
||||
Toast.show({ type: 'small', text1: 'Berhasil menambahkan anggota', })
|
||||
Toast.show({ type: 'small', text1: 'Berhasil menambahkan anggota' })
|
||||
dispatch(setUpdateDiscussionGeneralDetail(!update))
|
||||
router.back()
|
||||
} else {
|
||||
Toast.show({ type: 'small', text1: response.message, })
|
||||
Toast.show({ type: 'small', text1: response.message })
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
const message = error?.response?.data?.message || "Gagal menambahkan anggota"
|
||||
|
||||
Toast.show({ type: 'small', text1: message })
|
||||
console.error(error)
|
||||
Toast.show({ type: 'small', text1: error?.response?.data?.message || "Gagal menambahkan anggota" })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
||||
headerTitle: 'Tambah Anggota Diskusi',
|
||||
headerTitleAlign: 'center',
|
||||
// headerRight: () => (
|
||||
// <ButtonSaveHeader
|
||||
// category="update"
|
||||
// disable={selectMember.length == 0 || loading ? true : false}
|
||||
// onPress={() => {
|
||||
// handleAddMember()
|
||||
// }}
|
||||
// />
|
||||
// )
|
||||
header: () => (
|
||||
<AppHeader
|
||||
title="Tambah Anggota Diskusi"
|
||||
@@ -119,10 +194,8 @@ export default function AddMemberDiscussionDetail() {
|
||||
right={
|
||||
<ButtonSaveHeader
|
||||
category="update"
|
||||
disable={selectMember.length == 0 || loading ? true : false}
|
||||
onPress={() => {
|
||||
handleAddMember()
|
||||
}}
|
||||
disable={selectMember.length === 0 || loading}
|
||||
onPress={handleAddMember}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
@@ -131,65 +204,138 @@ export default function AddMemberDiscussionDetail() {
|
||||
/>
|
||||
<View style={[Styles.p15, Styles.flex1, { backgroundColor: colors.background }]}>
|
||||
<InputSearch onChange={setSearch} value={search} />
|
||||
|
||||
{
|
||||
selectMember.length > 0
|
||||
?
|
||||
<View>
|
||||
<ScrollView horizontal style={[Styles.mb10, Styles.pv10]} showsHorizontalScrollIndicator={false}>
|
||||
{
|
||||
selectMember.map((item: any, index: any) => (
|
||||
<ImageWithLabel
|
||||
key={index}
|
||||
label={item.name}
|
||||
src={`${ConstEnv.url_storage}/files/${item.img}`}
|
||||
onClick={() => onChoose(item.idUser, item.name, item.img)}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
:
|
||||
<Text style={[Styles.textDefault, Styles.pv05, Styles.textCenter, { color: colors.dimmed }]}>Tidak ada member yang dipilih</Text>
|
||||
}
|
||||
<ScrollView
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
|
||||
{
|
||||
data.length > 0 ?
|
||||
data.map((item: any, index: any) => {
|
||||
const found = dataOld.some((i: any) => i.idUser == item.id)
|
||||
return (
|
||||
<Pressable
|
||||
key={index}
|
||||
style={[Styles.itemSelectModal, { borderColor: colors.icon + '20' }]}
|
||||
onPress={() => {
|
||||
!found && onChoose(item.id, item.name, item.img)
|
||||
}}
|
||||
>
|
||||
<View style={[Styles.rowItemsCenter]}>
|
||||
<ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} border />
|
||||
<View style={[Styles.ml10]}>
|
||||
<Text style={[Styles.textDefault]}>{item.name}</Text>
|
||||
{
|
||||
found && <Text style={[Styles.textInformation, { color: colors.dimmed }]}>sudah menjadi anggota</Text>
|
||||
}
|
||||
{selectMember.length > 0 ? (
|
||||
<View>
|
||||
<ScrollView horizontal style={[Styles.mb10, Styles.pv10]} showsHorizontalScrollIndicator={false}>
|
||||
{selectMember.map((item, index) => (
|
||||
<ImageWithLabel
|
||||
key={index}
|
||||
label={item.name}
|
||||
src={`${ConstEnv.url_storage}/files/${item.img}`}
|
||||
onClick={() => handleToggleMember(item)}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
) : (
|
||||
<Text style={[Styles.textDefault, Styles.pv05, Styles.textCenter, { color: colors.dimmed }]}>
|
||||
Tidak ada member yang dipilih
|
||||
</Text>
|
||||
)}
|
||||
<ScrollView showsVerticalScrollIndicator={false}>
|
||||
<View>
|
||||
{search ? (
|
||||
loadingSearch ? (
|
||||
<ActivityIndicator color={colors.tabActive} style={{ marginTop: 20 }} />
|
||||
) : searchResults.length > 0 ? (
|
||||
searchResults.map((item, idx) => {
|
||||
const isOld = dataOld.some((o: any) => o.idUser === item.idUser)
|
||||
return (
|
||||
<Pressable
|
||||
key={idx}
|
||||
style={[Styles.itemSelectModal, { borderColor: colors.icon + '20' }]}
|
||||
onPress={() => !isOld && handleToggleMember(item)}
|
||||
>
|
||||
<View style={Styles.rowItemsCenter}>
|
||||
<ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} border />
|
||||
<View style={Styles.ml10}>
|
||||
<Text style={Styles.textDefault}>{item.name}</Text>
|
||||
{isOld && (
|
||||
<Text style={[Styles.textInformation, { color: colors.dimmed }]}>sudah menjadi anggota</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
{
|
||||
selectMember.some((i: any) => i.idUser == item.id) && <AntDesign name="check" size={20} color={colors.text} />
|
||||
}
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
{selectMember.some(s => s.idUser === item.idUser) && (
|
||||
<AntDesign name="check" size={18} color={colors.tabActive} />
|
||||
)}
|
||||
</Pressable>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<Text style={[Styles.textDefault, { textAlign: 'center', color: colors.dimmed, marginTop: 20 }]}>
|
||||
Tidak ada hasil
|
||||
</Text>
|
||||
)
|
||||
:
|
||||
<Text style={[Styles.textDefault, Styles.textCenter]}>Tidak ada data</Text>
|
||||
}
|
||||
) : loadingDivisions ? (
|
||||
<ActivityIndicator color={colors.tabActive} style={{ marginTop: 20 }} />
|
||||
) : divisions.length > 0 ? (
|
||||
divisions.map((division) => {
|
||||
const eligible = division.members.filter(m => !dataOld.some((o: any) => o.idUser === m.idUser))
|
||||
const selectedCount = eligible.filter(m => selectMember.some(s => s.idUser === m.idUser)).length
|
||||
const allSelected = division.membersLoaded && eligible.length > 0 && selectedCount === eligible.length
|
||||
const someSelected = selectedCount > 0 && !allSelected
|
||||
const isLoadingThis = loadingIds.includes(division.id)
|
||||
|
||||
return (
|
||||
<View key={division.id}>
|
||||
<Pressable
|
||||
style={[Styles.itemSelectModal, { borderColor: colors.icon + '20' }]}
|
||||
onPress={() => handleTapDivision(division)}
|
||||
>
|
||||
<View style={Styles.flex1}>
|
||||
<Text style={[Styles.textDefaultSemiBold, { color: colors.text }]}>{division.name}</Text>
|
||||
{division.membersLoaded && (
|
||||
<Text style={[Styles.textSmallSemiBold, { color: colors.dimmed }]}>
|
||||
{selectedCount > 0
|
||||
? `${selectedCount} dari ${eligible.length} dipilih`
|
||||
: `${eligible.length} anggota`}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
{isLoadingThis ? (
|
||||
<ActivityIndicator size="small" color={colors.dimmed} />
|
||||
) : allSelected ? (
|
||||
<AntDesign name="checkcircle" size={18} color={colors.tabActive} />
|
||||
) : someSelected ? (
|
||||
<AntDesign name="checkcircleo" size={18} color={colors.tabActive} />
|
||||
) : null}
|
||||
<Pressable
|
||||
onPress={() => handleToggleExpand(division.id)}
|
||||
style={{ paddingLeft: 10 }}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
>
|
||||
<Ionicons
|
||||
name={division.expanded ? "chevron-up" : "chevron-down"}
|
||||
size={16}
|
||||
color={colors.dimmed}
|
||||
/>
|
||||
</Pressable>
|
||||
</Pressable>
|
||||
|
||||
{division.expanded && division.members.map((member, idx) => {
|
||||
const isOld = dataOld.some((o: any) => o.idUser === member.idUser)
|
||||
return (
|
||||
<Pressable
|
||||
key={idx}
|
||||
style={[Styles.itemSelectModal, { borderColor: colors.icon + '15' }]}
|
||||
onPress={() => !isOld && handleToggleMember(member)}
|
||||
>
|
||||
<View style={Styles.rowItemsCenter}>
|
||||
<ImageUser src={`${ConstEnv.url_storage}/files/${member.img}`} border />
|
||||
<View style={Styles.ml10}>
|
||||
<Text style={Styles.textDefault}>{member.name}</Text>
|
||||
{isOld && (
|
||||
<Text style={[Styles.textInformation, { color: colors.dimmed }]}>sudah menjadi anggota</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
{!isOld && selectMember.some(s => s.idUser === member.idUser) && (
|
||||
<AntDesign name="check" size={18} color={colors.tabActive} />
|
||||
)}
|
||||
</Pressable>
|
||||
)
|
||||
})}
|
||||
</View>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<Text style={[Styles.textDefault, { textAlign: 'center', color: colors.dimmed, marginTop: 20 }]}>
|
||||
Tidak ada divisi
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import AppHeader from "@/components/AppHeader";
|
||||
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
||||
import ModalSelectMemberByDivision from "@/components/discussion_general/modalSelectMemberByDivision";
|
||||
import DrawerBottom from "@/components/drawerBottom";
|
||||
import ImageUser from "@/components/imageNew";
|
||||
import { InputForm } from "@/components/inputForm";
|
||||
@@ -50,10 +51,10 @@ export default function CreateDiscussionGeneral() {
|
||||
const userLogin = useSelector((state: any) => state.entities)
|
||||
const [chooseGroup, setChooseGroup] = useState({ val: "", label: "" });
|
||||
const [valChoose, setValChoose] = useState("")
|
||||
const [valSelect, setValSelect] = useState<"group" | "member">("group");
|
||||
const dispatch = useDispatch()
|
||||
const [disableBtn, setDisableBtn] = useState(true);
|
||||
const [isSelect, setSelect] = useState(false);
|
||||
const [isMemberModal, setMemberModal] = useState(false);
|
||||
const entitiesMember = useSelector((state: any) => state.memberChoose)
|
||||
const update = useSelector((state: any) => state.discussionGeneralDetailUpdate)
|
||||
const [loading, setLoading] = useState(false)
|
||||
@@ -90,16 +91,13 @@ export default function CreateDiscussionGeneral() {
|
||||
function handleOpenMemberPicker() {
|
||||
if (entityUser.role === "supadmin" || entityUser.role === "developer") {
|
||||
if (chooseGroup.val !== "") {
|
||||
setSelect(true);
|
||||
setValSelect("member");
|
||||
setMemberModal(true);
|
||||
} else {
|
||||
Toast.show({ type: 'small', text1: 'Pilih Lembaga Desa terlebih dahulu' })
|
||||
}
|
||||
} else {
|
||||
validationForm('group', userLogin.idGroup, userLogin.group);
|
||||
setValChoose(userLogin.idGroup)
|
||||
setSelect(true);
|
||||
setValSelect("member");
|
||||
setMemberModal(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +183,7 @@ export default function CreateDiscussionGeneral() {
|
||||
value={chooseGroup.label}
|
||||
required
|
||||
bg={colors.card}
|
||||
onPress={() => { setValChoose(chooseGroup.val); setValSelect("group"); setSelect(true) }}
|
||||
onPress={() => { setValChoose(chooseGroup.val); setSelect(true) }}
|
||||
error={error.group}
|
||||
errorText="Lembaga Desa tidak boleh kosong"
|
||||
/>
|
||||
@@ -305,14 +303,19 @@ export default function CreateDiscussionGeneral() {
|
||||
</ScrollView>
|
||||
|
||||
<ModalSelect
|
||||
category={valSelect}
|
||||
category="group"
|
||||
close={setSelect}
|
||||
onSelect={(value) => validationForm(valSelect, value.val, value.label)}
|
||||
title={valSelect === "group" ? "Lembaga Desa" : "Pilih Anggota"}
|
||||
onSelect={(value) => validationForm("group", value.val, value.label)}
|
||||
title="Lembaga Desa"
|
||||
open={isSelect}
|
||||
idParent={valSelect === "member" ? chooseGroup.val : ""}
|
||||
idParent=""
|
||||
valChoose={valChoose}
|
||||
/>
|
||||
<ModalSelectMemberByDivision
|
||||
open={isMemberModal}
|
||||
close={setMemberModal}
|
||||
idGroup={chooseGroup.val || userLogin.idGroup}
|
||||
/>
|
||||
|
||||
<DrawerBottom animation="slide" isVisible={isModalFile} setVisible={setModalFile} title="Menu">
|
||||
<View style={Styles.rowItemsCenter}>
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
import AppHeader from "@/components/AppHeader";
|
||||
import BorderBottomItem from "@/components/borderBottomItem";
|
||||
import DrawerBottom from "@/components/drawerBottom";
|
||||
import ImageUser from "@/components/imageNew";
|
||||
import MenuItemRow from "@/components/menuItemRow";
|
||||
import ModalConfirmation from "@/components/ModalConfirmation";
|
||||
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";
|
||||
import { apiDeleteMemberDiscussionGeneral, apiGetDiscussionGeneralOne } from "@/lib/api";
|
||||
import { useAuthSession } from "@/providers/AuthProvider";
|
||||
import { useTheme } from "@/providers/ThemeProvider";
|
||||
import { Feather, MaterialCommunityIcons } from "@expo/vector-icons";
|
||||
import { MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons";
|
||||
import { router, Stack, useLocalSearchParams } from "expo-router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { SafeAreaView, ScrollView, View } from "react-native";
|
||||
import { Pressable, SafeAreaView, ScrollView, View } from "react-native";
|
||||
import Toast from "react-native-toast-message";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
@@ -25,6 +22,8 @@ type Props = {
|
||||
img: string
|
||||
}
|
||||
|
||||
const SKELETON_COUNT = 5
|
||||
|
||||
export default function MemberDiscussionDetail() {
|
||||
const { token, decryptToken } = useAuthSession()
|
||||
const { colors } = useTheme();
|
||||
@@ -35,13 +34,12 @@ export default function MemberDiscussionDetail() {
|
||||
const [chooseUser, setChooseUser] = useState({ idUser: '', name: '', img: '' })
|
||||
const update = useSelector((state: any) => state.discussionGeneralDetailUpdate)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const arrSkeleton = Array.from({ length: 5 }, (_, index) => index)
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
||||
const canManage = entityUser.role !== "user" && entityUser.role !== "coadmin"
|
||||
|
||||
|
||||
async function handleLoad(loading: boolean) {
|
||||
async function handleLoad(showLoadingIndicator: boolean) {
|
||||
try {
|
||||
setLoading(loading)
|
||||
setLoading(showLoadingIndicator)
|
||||
const hasil = await decryptToken(String(token?.current))
|
||||
const response = await apiGetDiscussionGeneralOne({ id: id, user: hasil, cat: 'anggota' })
|
||||
setData(response.data)
|
||||
@@ -52,26 +50,18 @@ export default function MemberDiscussionDetail() {
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
handleLoad(false)
|
||||
}, [update]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
handleLoad(true)
|
||||
}, []);
|
||||
useEffect(() => { handleLoad(false) }, [update]);
|
||||
useEffect(() => { handleLoad(true) }, []);
|
||||
|
||||
async function handleDeleteUser() {
|
||||
try {
|
||||
const hasil = await decryptToken(String(token?.current))
|
||||
await apiDeleteMemberDiscussionGeneral({ user: hasil, idUser: chooseUser.idUser }, id)
|
||||
Toast.show({ type: 'small', text1: 'Berhasil mengeluarkan anggota dari diskusi', })
|
||||
Toast.show({ type: 'small', text1: 'Berhasil mengeluarkan anggota dari diskusi' })
|
||||
handleLoad(false)
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
const message = error?.response?.data?.message || "Gagal mengeluarkan anggota"
|
||||
|
||||
Toast.show({ type: 'small', text1: message })
|
||||
Toast.show({ type: 'small', text1: error?.response?.data?.message || "Gagal mengeluarkan anggota" })
|
||||
} finally {
|
||||
setModal(false)
|
||||
}
|
||||
@@ -81,9 +71,6 @@ export default function MemberDiscussionDetail() {
|
||||
<SafeAreaView style={[Styles.flex1, { backgroundColor: colors.background }]}>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
||||
headerTitle: 'Anggota Diskusi',
|
||||
headerTitleAlign: 'center',
|
||||
header: () => (
|
||||
<AppHeader
|
||||
title="Anggota Diskusi"
|
||||
@@ -93,49 +80,82 @@ export default function MemberDiscussionDetail() {
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<ScrollView style={[Styles.h100, Styles.flex1, { backgroundColor: colors.background }]}>
|
||||
<View style={[Styles.p15]}>
|
||||
<Text style={[Styles.textDefault, Styles.mv05]}>{data.length} Anggota</Text>
|
||||
<View style={[Styles.wrapPaper, { backgroundColor: colors.card, borderColor: colors.background }]}>
|
||||
{
|
||||
entityUser.role != "user" && entityUser.role != "coadmin" &&
|
||||
<BorderBottomItem
|
||||
onPress={() => { router.push(`/discussion/add-member/${id}`) }}
|
||||
borderType="none"
|
||||
icon={
|
||||
<View style={[Styles.iconContent, ColorsStatus.gray]}>
|
||||
<Feather name="user-plus" size={25} color={'#384288'} />
|
||||
<ScrollView showsVerticalScrollIndicator={false} style={[Styles.h100, Styles.flex1, { backgroundColor: colors.background }]}>
|
||||
<View style={[Styles.p15, Styles.mb100]}>
|
||||
|
||||
{/* Tombol tambah anggota */}
|
||||
{canManage && (
|
||||
<View style={[Styles.wrapPaper, Styles.sectionCard, Styles.mb15,
|
||||
{ backgroundColor: colors.card, borderColor: colors.icon + '18' }]}>
|
||||
<Pressable
|
||||
onPress={() => router.push(`/discussion/add-member/${id}`)}
|
||||
style={Styles.sectionActionRow}
|
||||
>
|
||||
<View style={[Styles.sectionIconBox, { backgroundColor: colors.icon + '18' }]}>
|
||||
<MaterialCommunityIcons name="account-plus-outline" size={18} color={colors.icon} />
|
||||
</View>
|
||||
<View style={Styles.flex1}>
|
||||
<Text style={[Styles.textDefaultSemiBold, { color: colors.text }]}>Tambah Anggota</Text>
|
||||
</View>
|
||||
<MaterialCommunityIcons name="chevron-right" size={18} color={colors.dimmed} />
|
||||
</Pressable>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Full list */}
|
||||
<View style={[Styles.wrapPaper, Styles.sectionCard,
|
||||
{ backgroundColor: colors.card, borderColor: colors.icon + '18', padding: 0, overflow: 'hidden' }]}>
|
||||
<View style={[Styles.sectionActionRow, { padding: 16, borderBottomWidth: 1, borderBottomColor: colors.icon + '14' }]}>
|
||||
<View style={[Styles.sectionIconBox, { backgroundColor: colors.dimmed + '18' }]}>
|
||||
<MaterialIcons name="people" size={18} color={colors.dimmed} />
|
||||
</View>
|
||||
<Text style={[Styles.textDefaultSemiBold, Styles.flex1, { color: colors.text }]}>Anggota</Text>
|
||||
{!loading && (
|
||||
<Text style={[Styles.textMediumNormal, { color: colors.dimmed }]}>{data.length} anggota</Text>
|
||||
)}
|
||||
</View>
|
||||
{loading
|
||||
? Array.from({ length: SKELETON_COUNT }).map((_, i) => (
|
||||
<View
|
||||
key={i}
|
||||
style={[Styles.rowItemsCenter, Styles.ph15,
|
||||
{ paddingVertical: 14, gap: 14, borderBottomWidth: i < SKELETON_COUNT - 1 ? 1 : 0, borderBottomColor: colors.icon + '14' }]}
|
||||
>
|
||||
<View style={[Styles.userProfileExtraSmall, { backgroundColor: colors.icon + '20', borderRadius: 100 }]} />
|
||||
<View style={{ height: 13, borderRadius: 6, flex: 1, backgroundColor: colors.icon + '20', maxWidth: 140 + (i % 3) * 30 }} />
|
||||
</View>
|
||||
))
|
||||
: data.length === 0
|
||||
? (
|
||||
<View style={[Styles.contentItemCenter, { paddingVertical: 40 }]}>
|
||||
<MaterialIcons name="people-outline" size={34} color={colors.icon + '50'} />
|
||||
<Text style={[Styles.textMediumNormal, Styles.mt10, { color: colors.dimmed }]}>Belum ada anggota</Text>
|
||||
</View>
|
||||
}
|
||||
title="Tambah Anggota"
|
||||
/>
|
||||
}
|
||||
{
|
||||
loading ?
|
||||
arrSkeleton.map((item, index) => {
|
||||
return (
|
||||
<SkeletonTwoItem key={index} />
|
||||
)
|
||||
})
|
||||
:
|
||||
data.map((item, index) => {
|
||||
return (
|
||||
<BorderBottomItem
|
||||
key={index}
|
||||
borderType="bottom"
|
||||
icon={
|
||||
<ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} size="sm" />
|
||||
}
|
||||
title={item.name}
|
||||
onPress={() => {
|
||||
setChooseUser(item)
|
||||
setModal(true)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
)
|
||||
: data.map((item, index) => (
|
||||
<Pressable
|
||||
key={index}
|
||||
onPress={() => { setChooseUser(item); setModal(true) }}
|
||||
style={({ pressed }) => [
|
||||
Styles.rowItemsCenter, Styles.ph15,
|
||||
{
|
||||
paddingVertical: 13, gap: 14,
|
||||
borderBottomWidth: index < data.length - 1 ? 1 : 0,
|
||||
borderBottomColor: colors.icon + '14',
|
||||
backgroundColor: pressed ? colors.icon + '0E' : 'transparent',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} size="xs" />
|
||||
<Text style={[Styles.textDefault, Styles.flex1, { color: colors.text }]} numberOfLines={1}>
|
||||
{item.name}
|
||||
</Text>
|
||||
<MaterialCommunityIcons name="chevron-right" size={18} color={colors.icon + '60'} />
|
||||
</Pressable>
|
||||
))
|
||||
}
|
||||
</View>
|
||||
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
@@ -149,20 +169,16 @@ export default function MemberDiscussionDetail() {
|
||||
router.push(`/member/${chooseUser.idUser}`)
|
||||
}}
|
||||
/>
|
||||
{
|
||||
entityUser.role != "user" && entityUser.role != "coadmin" &&
|
||||
{canManage && (
|
||||
<MenuItemRow
|
||||
icon={<MaterialCommunityIcons name="account-remove" color={colors.text} size={25} />}
|
||||
title="Keluarkan"
|
||||
onPress={() => {
|
||||
setModal(false)
|
||||
setTimeout(() => {
|
||||
setShowDeleteModal(true)
|
||||
}, 600)
|
||||
setTimeout(() => setShowDeleteModal(true), 600)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
||||
)}
|
||||
</View>
|
||||
</DrawerBottom>
|
||||
|
||||
@@ -170,10 +186,7 @@ export default function MemberDiscussionDetail() {
|
||||
visible={showDeleteModal}
|
||||
title="Konfirmasi"
|
||||
message="Apakah anda yakin ingin mengeluarkan anggota?"
|
||||
onConfirm={() => {
|
||||
setShowDeleteModal(false)
|
||||
handleDeleteUser()
|
||||
}}
|
||||
onConfirm={() => { setShowDeleteModal(false); handleDeleteUser() }}
|
||||
onCancel={() => setShowDeleteModal(false)}
|
||||
confirmText="Hapus"
|
||||
cancelText="Batal"
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import ModalConfirmation from "@/components/ModalConfirmation"
|
||||
import AppHeader from "@/components/AppHeader"
|
||||
import BorderBottomItem from "@/components/borderBottomItem"
|
||||
import ButtonBackHeader from "@/components/buttonBackHeader"
|
||||
import HeaderRightCalendarDetail from "@/components/calendar/headerCalendarDetail"
|
||||
import DrawerBottom from "@/components/drawerBottom"
|
||||
import HeaderRightCalendarDetail from "@/components/calendar/headerCalendarDetail"
|
||||
import ImageUser from "@/components/imageNew"
|
||||
import MenuItemRow from "@/components/menuItemRow"
|
||||
import Skeleton from "@/components/skeleton"
|
||||
import ModalConfirmation from "@/components/ModalConfirmation"
|
||||
import Text from "@/components/Text"
|
||||
import { ConstEnv } from "@/constants/ConstEnv"
|
||||
import Styles from "@/constants/Styles"
|
||||
@@ -14,7 +11,7 @@ import { apiDeleteCalendarMember, apiGetCalendarOne, apiGetDivisionOneFeature }
|
||||
import { setUpdateCalendar } from "@/lib/calendarUpdate"
|
||||
import { useAuthSession } from "@/providers/AuthProvider"
|
||||
import { useTheme } from "@/providers/ThemeProvider"
|
||||
import { MaterialCommunityIcons } from "@expo/vector-icons"
|
||||
import { MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons"
|
||||
import Clipboard from "@react-native-clipboard/clipboard"
|
||||
import { router, Stack, useLocalSearchParams } from "expo-router"
|
||||
import { useEffect, useState } from "react"
|
||||
@@ -156,135 +153,142 @@ export default function DetailEventCalendar() {
|
||||
setRefreshing(false)
|
||||
};
|
||||
|
||||
const canManage = !((entityUser.role === "user" || entityUser.role === "coadmin") && !isMemberDivision)
|
||||
|
||||
const repeatLabel: Record<string, string> = {
|
||||
once: 'Acara 1 Kali',
|
||||
daily: 'Setiap Hari',
|
||||
weekly: 'Mingguan',
|
||||
monthly: 'Bulanan',
|
||||
yearly: 'Tahunan',
|
||||
}
|
||||
|
||||
function InfoRow({ icon, label, value, onCopy }: { icon: string, label: string, value?: string, onCopy?: () => void }) {
|
||||
return (
|
||||
<View style={[Styles.sectionActionRow, { paddingVertical: 10, borderBottomWidth: 1, borderBottomColor: colors.icon + '14' }]}>
|
||||
<View style={[Styles.sectionIconBox, { backgroundColor: colors.dimmed + '18' }]}>
|
||||
<MaterialCommunityIcons name={icon as any} size={18} color={colors.dimmed} />
|
||||
</View>
|
||||
<View style={Styles.flex1}>
|
||||
<Text style={[Styles.textSmallSemiBold, { color: colors.dimmed, marginBottom: 2 }]}>{label}</Text>
|
||||
{loading
|
||||
? <View style={{ height: 13, borderRadius: 6, backgroundColor: colors.icon + '20', width: '70%' }} />
|
||||
: <Text style={[Styles.textDefault, { color: colors.text }]}>{value || '-'}</Text>
|
||||
}
|
||||
</View>
|
||||
{onCopy && !loading && value && (
|
||||
<Pressable onPress={onCopy} style={{ padding: 4 }}>
|
||||
<MaterialCommunityIcons name="content-copy" size={16} color={colors.dimmed} />
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{ flex: 1, backgroundColor: colors.background }}>
|
||||
<SafeAreaView style={[Styles.flex1, { backgroundColor: colors.background }]}>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
||||
headerTitle: 'Detail Acara',
|
||||
headerTitleAlign: 'center',
|
||||
// headerRight: () => (entityUser.role == "user" || entityUser.role == "coadmin") && !isMemberDivision ? <></> : <HeaderRightCalendarDetail id={String(data?.idCalendar)} idReminder={String(detail)} />
|
||||
header: () => (
|
||||
<AppHeader
|
||||
title="Detail Acara"
|
||||
showBack={true}
|
||||
onPressLeft={() => router.back()}
|
||||
right={
|
||||
(entityUser.role == "user" || entityUser.role == "coadmin") && !isMemberDivision ? <></> : <HeaderRightCalendarDetail id={String(data?.idCalendar)} idReminder={String(detail)} />
|
||||
(entityUser.role === "user" || entityUser.role === "coadmin") && !isMemberDivision
|
||||
? <></> : <HeaderRightCalendarDetail id={String(data?.idCalendar)} idReminder={String(detail)} />
|
||||
}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<ScrollView
|
||||
style={[Styles.h100]}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={handleRefresh}
|
||||
tintColor={colors.icon}
|
||||
/>
|
||||
}
|
||||
showsVerticalScrollIndicator={false}
|
||||
style={Styles.h100}
|
||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} tintColor={colors.icon} />}
|
||||
>
|
||||
<View style={[Styles.p15]}>
|
||||
<View style={[Styles.wrapPaper, Styles.mb15, { backgroundColor: colors.card, borderColor: colors.background }]}>
|
||||
<View style={[Styles.rowItemsCenter, { alignItems: 'flex-start' }]}>
|
||||
<MaterialCommunityIcons name="calendar-text" size={30} color={colors.text} style={Styles.mr10} />
|
||||
{
|
||||
loading ?
|
||||
<Skeleton width={80} height={10} borderRadius={10} widthType="percent" />
|
||||
: <Text style={[Styles.textDefault, Styles.w90]}>{data?.title}</Text>
|
||||
}
|
||||
<View style={[Styles.p15, Styles.mb100]}>
|
||||
|
||||
{/* Info acara */}
|
||||
<View style={[Styles.wrapPaper, Styles.sectionCard, Styles.noShadow, Styles.mb15,
|
||||
{ backgroundColor: colors.card, borderColor: colors.icon + '18', padding: 0, overflow: 'hidden' }]}>
|
||||
<View style={{ padding: 16, borderBottomWidth: 1, borderBottomColor: colors.icon + '14' }}>
|
||||
<View style={Styles.sectionActionRow}>
|
||||
<View style={[Styles.sectionIconBox, { backgroundColor: colors.dimmed + '18' }]}>
|
||||
<MaterialCommunityIcons name="calendar-text" size={18} color={colors.dimmed} />
|
||||
</View>
|
||||
<Text style={[Styles.textDefaultSemiBold, Styles.flex1, { color: colors.text }]}>Detail Acara</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={[Styles.rowItemsCenter, Styles.mt10]}>
|
||||
<MaterialCommunityIcons name="calendar-month-outline" size={30} color={colors.text} style={Styles.mr10} />
|
||||
{
|
||||
loading ?
|
||||
<Skeleton width={80} height={10} borderRadius={10} widthType="percent" />
|
||||
:
|
||||
<Text style={[Styles.textDefault]}>{data?.dateStart}</Text>
|
||||
}
|
||||
<View style={{ paddingHorizontal: 16 }}>
|
||||
<InfoRow icon="format-title" label="Judul" value={data?.title} />
|
||||
<InfoRow icon="calendar-month-outline" label="Tanggal" value={data?.dateStart} />
|
||||
<InfoRow icon="clock-outline" label="Waktu" value={data ? `${data.timeStart} – ${data.timeEnd}` : undefined} />
|
||||
<InfoRow icon="repeat" label="Pengulangan" value={data?.repeatEventTyper ? repeatLabel[data.repeatEventTyper] : undefined} />
|
||||
<InfoRow icon="link-variant" label="Link Meet" value={data?.linkMeet} onCopy={data?.linkMeet ? () => handleCopy(data.linkMeet) : undefined} />
|
||||
<View style={[Styles.sectionActionRow, { paddingVertical: 10, alignItems: 'flex-start' }]}>
|
||||
<View style={[Styles.sectionIconBox, { backgroundColor: colors.dimmed + '18' }]}>
|
||||
<MaterialCommunityIcons name="card-text-outline" size={18} color={colors.dimmed} />
|
||||
</View>
|
||||
<View style={Styles.flex1}>
|
||||
<Text style={[Styles.textSmallSemiBold, { color: colors.dimmed, marginBottom: 2 }]}>Deskripsi</Text>
|
||||
{loading
|
||||
? <View style={{ height: 13, borderRadius: 6, backgroundColor: colors.icon + '20', width: '80%' }} />
|
||||
: <Text style={[Styles.textDefault, { color: colors.text, lineHeight: 22 }]}>{data?.desc || '-'}</Text>
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View style={[Styles.rowItemsCenter, Styles.mt10]}>
|
||||
<MaterialCommunityIcons name="clock-outline" size={30} color={colors.text} style={Styles.mr10} />
|
||||
{
|
||||
loading ?
|
||||
<Skeleton width={80} height={10} borderRadius={10} widthType="percent" />
|
||||
:
|
||||
<Text style={[Styles.textDefault]}>{data?.timeStart} | {data?.timeEnd}</Text>
|
||||
}
|
||||
</View>
|
||||
|
||||
{/* Daftar anggota */}
|
||||
<View style={[Styles.wrapPaper, Styles.sectionCard, Styles.noShadow,
|
||||
{ backgroundColor: colors.card, borderColor: colors.icon + '18', padding: 0, overflow: 'hidden' }]}>
|
||||
|
||||
<View style={[Styles.sectionActionRow, { padding: 16, borderBottomWidth: 1, borderBottomColor: colors.icon + '14' }]}>
|
||||
<View style={[Styles.sectionIconBox, { backgroundColor: colors.dimmed + '18' }]}>
|
||||
<MaterialIcons name="people" size={18} color={colors.dimmed} />
|
||||
</View>
|
||||
<Text style={[Styles.textDefaultSemiBold, Styles.flex1, { color: colors.text }]}>Anggota</Text>
|
||||
<Text style={[Styles.textMediumNormal, { color: colors.dimmed }]}>{member.length} anggota</Text>
|
||||
</View>
|
||||
<View style={[Styles.rowItemsCenter, Styles.mt10]}>
|
||||
<MaterialCommunityIcons name="repeat" size={30} color={colors.text} style={Styles.mr10} />
|
||||
{
|
||||
loading ?
|
||||
<Skeleton width={80} height={10} borderRadius={10} widthType="percent" />
|
||||
:
|
||||
<Text style={[Styles.textDefault]}>
|
||||
|
||||
{member.length === 0
|
||||
? (
|
||||
<View style={[Styles.contentItemCenter, { paddingVertical: 40 }]}>
|
||||
<MaterialIcons name="people-outline" size={34} color={colors.icon + '50'} />
|
||||
<Text style={[Styles.textMediumNormal, Styles.mt10, { color: colors.dimmed }]}>Belum ada anggota</Text>
|
||||
</View>
|
||||
)
|
||||
: member.map((item, index) => (
|
||||
<Pressable
|
||||
key={index}
|
||||
onPress={() => {
|
||||
if (!canManage) return
|
||||
setMemberChoose({ id: item.idUser, name: item.name })
|
||||
setModalMember(true)
|
||||
}}
|
||||
style={({ pressed }) => [
|
||||
Styles.rowItemsCenter, Styles.ph15,
|
||||
{
|
||||
data?.repeatEventTyper.toString() === 'once' ? 'Acara 1 Kali' :
|
||||
data?.repeatEventTyper.toString() === 'daily' ? 'Setiap Hari' :
|
||||
data?.repeatEventTyper.toString() === 'weekly' ? 'Mingguan' :
|
||||
data?.repeatEventTyper.toString() === 'monthly' ? 'Bulanan' :
|
||||
data?.repeatEventTyper.toString() === 'yearly' ? 'Tahunan' :
|
||||
''
|
||||
}
|
||||
</Text>
|
||||
}
|
||||
</View>
|
||||
<View style={[Styles.rowItemsCenter, Styles.mt10]}>
|
||||
<MaterialCommunityIcons name="link-variant" size={30} color={colors.text} style={Styles.mr10} />
|
||||
{
|
||||
loading ?
|
||||
<Skeleton width={80} height={10} borderRadius={10} widthType="percent" />
|
||||
:
|
||||
data?.linkMeet ?
|
||||
<Pressable onPress={() => { handleCopy(data.linkMeet) }}>
|
||||
<Text style={[Styles.textDefault]}>{data.linkMeet}</Text>
|
||||
</Pressable>
|
||||
: <Text style={[Styles.textDefault]}>-</Text>
|
||||
}
|
||||
</View>
|
||||
<View style={[Styles.rowItemsCenter, Styles.mt10, { alignItems: 'flex-start' }]}>
|
||||
<MaterialCommunityIcons name="card-text-outline" size={30} color={colors.text} style={Styles.mr10} />
|
||||
{
|
||||
loading ?
|
||||
<Skeleton width={80} height={10} borderRadius={10} widthType="percent" />
|
||||
:
|
||||
<Text style={[Styles.textDefault, Styles.w90]}>{data?.desc}</Text>
|
||||
}
|
||||
</View>
|
||||
paddingVertical: 12, gap: 14,
|
||||
borderBottomWidth: index < member.length - 1 ? 1 : 0,
|
||||
borderBottomColor: colors.icon + '14',
|
||||
backgroundColor: pressed && canManage ? colors.icon + '0E' : 'transparent',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} size="xs" />
|
||||
<View style={Styles.flex1}>
|
||||
<Text style={[Styles.textDefault, { color: colors.text }]} numberOfLines={1}>{item.name}</Text>
|
||||
<Text style={[Styles.textMediumNormal, { color: colors.dimmed }]} numberOfLines={1}>{item.email}</Text>
|
||||
</View>
|
||||
{canManage && <MaterialCommunityIcons name="chevron-right" size={18} color={colors.icon + '60'} />}
|
||||
</Pressable>
|
||||
))
|
||||
}
|
||||
</View>
|
||||
|
||||
<View style={[Styles.mb15]}>
|
||||
<View style={[Styles.rowSpaceBetween, Styles.mv05]}>
|
||||
<Text style={[Styles.textDefaultSemiBold]}>Anggota</Text>
|
||||
<Text style={[Styles.textDefault]}>Total {member.length} Anggota</Text>
|
||||
</View>
|
||||
|
||||
<View style={[Styles.wrapPaper, { backgroundColor: colors.card, borderColor: colors.background }]}>
|
||||
{
|
||||
member.map((item, index) => (
|
||||
<BorderBottomItem
|
||||
key={index}
|
||||
borderType="bottom"
|
||||
icon={<ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} />}
|
||||
title={item.name}
|
||||
subtitle={item.email}
|
||||
onPress={() => {
|
||||
if ((entityUser.role == "user" || entityUser.role == "coadmin") && !isMemberDivision) {
|
||||
null
|
||||
} else {
|
||||
setMemberChoose({ id: item.idUser, name: item.name })
|
||||
setModalMember(true)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
|
||||
@@ -74,6 +74,22 @@ export default function AddMemberTask() {
|
||||
}
|
||||
}
|
||||
|
||||
const availableData = data.filter((item: any) => !dataOld.some((i: any) => i.idUser == item.idUser))
|
||||
const isAllSelected = availableData.length > 0 && availableData.every((item: any) =>
|
||||
selectMember.some((s: any) => s.idUser == item.idUser)
|
||||
)
|
||||
|
||||
function handleSelectAll() {
|
||||
if (isAllSelected) {
|
||||
setSelectMember([])
|
||||
} else {
|
||||
const newMembers = availableData
|
||||
.filter((item: any) => !selectMember.some((s: any) => s.idUser == item.idUser))
|
||||
.map((item: any) => ({ idUser: item.idUser, name: item.name, img: item.img }))
|
||||
setSelectMember([...selectMember, ...newMembers])
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAddMember() {
|
||||
try {
|
||||
setLoading(true)
|
||||
@@ -159,6 +175,15 @@ export default function AddMemberTask() {
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
|
||||
{availableData.length > 0 && (
|
||||
<Pressable
|
||||
style={[Styles.itemSelectModal, { borderColor: colors.icon + '20', paddingTop: 0 }]}
|
||||
onPress={handleSelectAll}
|
||||
>
|
||||
<Text style={[Styles.textMediumSemiBold]}>{isAllSelected ? 'Batalkan Semua' : 'Pilih Semua'}</Text>
|
||||
{isAllSelected && <AntDesign name="check" size={20} color={colors.text} />}
|
||||
</Pressable>
|
||||
)}
|
||||
{
|
||||
data.length > 0 ?
|
||||
data.map((item: any, index: any) => {
|
||||
|
||||
@@ -54,6 +54,21 @@ export default function AddMemberCreateTask() {
|
||||
}
|
||||
}
|
||||
|
||||
const isAllSelected = data.length > 0 && data.every((item: any) =>
|
||||
selectMember.some((s: any) => s.idUser == item.idUser)
|
||||
)
|
||||
|
||||
function handleSelectAll() {
|
||||
if (isAllSelected) {
|
||||
setSelectMember([])
|
||||
} else {
|
||||
const newMembers = data
|
||||
.filter((item: any) => !selectMember.some((s: any) => s.idUser == item.idUser))
|
||||
.map((item: any) => ({ idUser: item.idUser, name: item.name, img: item.img }))
|
||||
setSelectMember([...selectMember, ...newMembers])
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAddMember() {
|
||||
try {
|
||||
dispatch(setMemberChoose(selectMember))
|
||||
@@ -127,6 +142,15 @@ export default function AddMemberCreateTask() {
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
|
||||
{data.length > 0 && (
|
||||
<Pressable
|
||||
style={[Styles.itemSelectModal, { borderColor: colors.icon + '20', paddingTop: 0 }]}
|
||||
onPress={handleSelectAll}
|
||||
>
|
||||
<Text style={[Styles.textMediumSemiBold]}>{isAllSelected ? 'Batalkan Semua' : 'Pilih Semua'}</Text>
|
||||
{isAllSelected && <AntDesign name="check" size={20} color={colors.text} />}
|
||||
</Pressable>
|
||||
)}
|
||||
{
|
||||
data.length > 0 ?
|
||||
data.map((item: any, index: any) => {
|
||||
|
||||
@@ -77,6 +77,22 @@ export default function AddMemberDivision() {
|
||||
}
|
||||
}
|
||||
|
||||
const availableData = data.filter((item: any) => !dataOld.some((i: any) => i.idUser == item.id))
|
||||
const isAllSelected = availableData.length > 0 && availableData.every((item: any) =>
|
||||
selectMember.some((s: any) => s.idUser == item.id)
|
||||
)
|
||||
|
||||
function handleSelectAll() {
|
||||
if (isAllSelected) {
|
||||
setSelectMember([])
|
||||
} else {
|
||||
const newMembers = availableData
|
||||
.filter((item: any) => !selectMember.some((s: any) => s.idUser == item.id))
|
||||
.map((item: any) => ({ idUser: item.id, name: item.name, img: item.img }))
|
||||
setSelectMember([...selectMember, ...newMembers])
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAddMember() {
|
||||
try {
|
||||
setLoading(true)
|
||||
@@ -141,7 +157,7 @@ export default function AddMemberDivision() {
|
||||
selectMember.length > 0
|
||||
?
|
||||
<View>
|
||||
<ScrollView horizontal style={[Styles.mb10, Styles.pv10]} showsHorizontalScrollIndicator={false}>
|
||||
<ScrollView horizontal style={[Styles.mb05, Styles.pv10]} showsHorizontalScrollIndicator={false}>
|
||||
{
|
||||
selectMember.map((item: any, index: any) => (
|
||||
<ImageWithLabel
|
||||
@@ -162,6 +178,15 @@ export default function AddMemberDivision() {
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
|
||||
{availableData.length > 0 && (
|
||||
<Pressable
|
||||
style={[Styles.itemSelectModal, { borderColor: colors.icon + '20', paddingTop: 0 }]}
|
||||
onPress={handleSelectAll}
|
||||
>
|
||||
<Text style={[Styles.textMediumSemiBold]}>{isAllSelected ? 'Batalkan Semua' : 'Pilih Semua'}</Text>
|
||||
{isAllSelected && <AntDesign name="check" size={20} color={colors.text} />}
|
||||
</Pressable>
|
||||
)}
|
||||
{
|
||||
data.length > 0 ?
|
||||
data.map((item: any, index: any) => {
|
||||
@@ -169,7 +194,7 @@ export default function AddMemberDivision() {
|
||||
return (
|
||||
<Pressable
|
||||
key={index}
|
||||
style={[Styles.itemSelectModal]}
|
||||
style={[Styles.itemSelectModal, { borderBottomColor: colors.icon + '20' }]}
|
||||
onPress={() => {
|
||||
!found && onChoose(item.id, item.name, item.img)
|
||||
}}
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
import ModalConfirmation from "@/components/ModalConfirmation"
|
||||
import AppHeader from "@/components/AppHeader"
|
||||
import BorderBottomItem from "@/components/borderBottomItem"
|
||||
import HeaderRightDivisionInfo from "@/components/division/headerDivisionInfo"
|
||||
import DrawerBottom from "@/components/drawerBottom"
|
||||
import HeaderRightDivisionInfo from "@/components/division/headerDivisionInfo"
|
||||
import ImageUser from "@/components/imageNew"
|
||||
import MenuItemRow from "@/components/menuItemRow"
|
||||
import ModalConfirmation from "@/components/ModalConfirmation"
|
||||
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"
|
||||
import { apiDeleteMemberDivision, apiGetDivisionOneDetail, apiGetDivisionOneFeature, apiUpdateStatusAdminDivision } from "@/lib/api"
|
||||
import { useAuthSession } from "@/providers/AuthProvider"
|
||||
import { useTheme } from "@/providers/ThemeProvider"
|
||||
import { Feather, MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons"
|
||||
import { MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons"
|
||||
import { router, Stack, useLocalSearchParams } from "expo-router"
|
||||
import { useEffect, useState } from "react"
|
||||
import { Pressable, RefreshControl, SafeAreaView, ScrollView, View } from "react-native"
|
||||
@@ -50,8 +47,8 @@ export default function InformationDivision() {
|
||||
const [dataMember, setDataMember] = useState<PropsMember[]>([])
|
||||
const [refresh, setRefresh] = useState(false)
|
||||
const update = useSelector((state: any) => state.divisionUpdate)
|
||||
const arrSkeleton = Array.from({ length: 5 }, (_, index) => index)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const SKELETON_COUNT = 5
|
||||
const [isMemberDivision, setIsMemberDivision] = useState(false)
|
||||
const [isAdminDivision, setIsAdminDivision] = useState(false)
|
||||
const [dataMemberChoose, setDataMemberChoose] = useState({
|
||||
@@ -186,109 +183,123 @@ export default function InformationDivision() {
|
||||
}}
|
||||
/>
|
||||
<ScrollView
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={handleRefresh}
|
||||
tintColor={colors.icon}
|
||||
/>
|
||||
}
|
||||
showsVerticalScrollIndicator={false}
|
||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} tintColor={colors.icon} />}
|
||||
style={[Styles.h100, { backgroundColor: colors.background }]}
|
||||
>
|
||||
<View style={[Styles.p15]}>
|
||||
{
|
||||
dataDetail?.isActive == false && (
|
||||
<SectionCancel title={'Divisi nonaktif'} />
|
||||
)
|
||||
}
|
||||
<View style={[Styles.mb15]}>
|
||||
<Text style={[Styles.textDefaultSemiBold, Styles.mb05]}>Deskripsi Divisi</Text>
|
||||
<View style={[Styles.wrapPaper, Styles.noShadow, { backgroundColor: colors.card, borderWidth: 1, borderColor: colors.icon + '20' }]}>
|
||||
{loading ?
|
||||
arrSkeleton.map((item, index) => {
|
||||
<View style={[Styles.p15, Styles.mb100]}>
|
||||
|
||||
{dataDetail?.isActive === false && <SectionCancel title="Divisi nonaktif" />}
|
||||
|
||||
{/* Deskripsi */}
|
||||
<View style={[Styles.wrapPaper, Styles.sectionCard, Styles.noShadow, Styles.mb15,
|
||||
{ backgroundColor: colors.card, borderColor: colors.icon + '18' }]}>
|
||||
<View style={[Styles.sectionActionRow, { marginBottom: 12 }]}>
|
||||
<View style={[Styles.sectionIconBox, { backgroundColor: colors.dimmed + '18' }]}>
|
||||
<MaterialIcons name="info-outline" size={18} color={colors.dimmed} />
|
||||
</View>
|
||||
<Text style={[Styles.textDefaultSemiBold, Styles.flex1, { color: colors.text }]}>Deskripsi</Text>
|
||||
</View>
|
||||
{loading
|
||||
? Array.from({ length: 3 }).map((_, i) => (
|
||||
<View key={i} style={{ height: 13, borderRadius: 6, marginBottom: 8, backgroundColor: colors.icon + '20', width: i === 2 ? '60%' : '100%' }} />
|
||||
))
|
||||
: <Text style={[Styles.textDefault, { color: colors.text, lineHeight: 22 }]}>{dataDetail?.desc}</Text>
|
||||
}
|
||||
</View>
|
||||
|
||||
{/* Tombol tambah anggota */}
|
||||
{((entityUser.role !== "user" && entityUser.role !== "coadmin") || isAdminDivision) && dataDetail?.isActive && (
|
||||
<View style={[Styles.wrapPaper, Styles.sectionCard, Styles.mb15,
|
||||
{ backgroundColor: colors.card, borderColor: colors.icon + '18' }]}>
|
||||
<Pressable
|
||||
onPress={() => router.push(`/division/${id}/add-member`)}
|
||||
style={Styles.sectionActionRow}
|
||||
>
|
||||
<View style={[Styles.sectionIconBox, { backgroundColor: colors.icon + '18' }]}>
|
||||
<MaterialCommunityIcons name="account-plus-outline" size={18} color={colors.icon} />
|
||||
</View>
|
||||
<Text style={[Styles.textDefaultSemiBold, Styles.flex1, { color: colors.text }]}>Tambah Anggota</Text>
|
||||
<MaterialCommunityIcons name="chevron-right" size={18} color={colors.dimmed} />
|
||||
</Pressable>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Daftar anggota */}
|
||||
<View style={[Styles.wrapPaper, Styles.sectionCard,
|
||||
{ backgroundColor: colors.card, borderColor: colors.icon + '18', padding: 0, overflow: 'hidden' }]}>
|
||||
|
||||
{/* Header */}
|
||||
<View style={[Styles.sectionActionRow, { padding: 16, borderBottomWidth: 1, borderBottomColor: colors.icon + '14' }]}>
|
||||
<View style={[Styles.sectionIconBox, { backgroundColor: colors.dimmed + '18' }]}>
|
||||
<MaterialIcons name="people" size={18} color={colors.dimmed} />
|
||||
</View>
|
||||
<Text style={[Styles.textDefaultSemiBold, Styles.flex1, { color: colors.text }]}>Anggota</Text>
|
||||
{!loading && (
|
||||
<Text style={[Styles.textMediumNormal, { color: colors.dimmed }]}>{dataMember.length} anggota</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{loading
|
||||
? Array.from({ length: SKELETON_COUNT }).map((_, i) => (
|
||||
<View key={i} style={[Styles.rowItemsCenter, Styles.ph15,
|
||||
{ paddingVertical: 14, gap: 14, borderBottomWidth: i < SKELETON_COUNT - 1 ? 1 : 0, borderBottomColor: colors.icon + '14' }]}>
|
||||
<View style={[Styles.userProfileExtraSmall, { backgroundColor: colors.icon + '20', borderRadius: 100 }]} />
|
||||
<View style={{ height: 13, borderRadius: 6, flex: 1, backgroundColor: colors.icon + '20', maxWidth: 140 + (i % 3) * 30 }} />
|
||||
</View>
|
||||
))
|
||||
: dataMember.length === 0
|
||||
? (
|
||||
<View style={[Styles.contentItemCenter, { paddingVertical: 40 }]}>
|
||||
<MaterialIcons name="people-outline" size={34} color={colors.icon + '50'} />
|
||||
<Text style={[Styles.textMediumNormal, Styles.mt10, { color: colors.dimmed }]}>Belum ada anggota</Text>
|
||||
</View>
|
||||
)
|
||||
: dataMember.map((item, index) => {
|
||||
const canPress = dataDetail?.isActive && (isAdminDivision || (entityUser.role !== "user" && entityUser.role !== "coadmin"))
|
||||
return (
|
||||
<Skeleton key={index} width={100} height={10} widthType="percent" borderRadius={10} />
|
||||
<Pressable
|
||||
key={index}
|
||||
onPress={() => canPress && handleChooseMember(item)}
|
||||
style={({ pressed }) => [
|
||||
Styles.rowItemsCenter, Styles.ph15,
|
||||
{
|
||||
paddingVertical: 13, gap: 14,
|
||||
borderBottomWidth: index < dataMember.length - 1 ? 1 : 0,
|
||||
borderBottomColor: colors.icon + '14',
|
||||
backgroundColor: pressed && canPress ? colors.icon + '0E' : 'transparent',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} size="xs" />
|
||||
<Text style={[Styles.textDefault, Styles.flex1, { color: colors.text }]} numberOfLines={1}>
|
||||
{item.name}
|
||||
</Text>
|
||||
<Text style={[Styles.textMediumNormal, { color: item.isAdmin ? colors.tabActive : colors.dimmed }]}>
|
||||
{item.isAdmin ? 'Admin' : 'Anggota'}
|
||||
</Text>
|
||||
{canPress && <MaterialCommunityIcons name="chevron-right" size={18} color={colors.icon + '60'} />}
|
||||
</Pressable>
|
||||
)
|
||||
})
|
||||
:
|
||||
<Text>{dataDetail?.desc}</Text>
|
||||
}
|
||||
</View>
|
||||
}
|
||||
</View>
|
||||
<View style={[Styles.mb15]}>
|
||||
<Text style={[Styles.textDefault, Styles.mv05]}>{dataMember.length} Anggota</Text>
|
||||
<View style={[Styles.wrapPaper, Styles.noShadow, { backgroundColor: colors.card, borderWidth: 1, borderColor: colors.icon + '20' }]}>
|
||||
{
|
||||
((entityUser.role != "user" && entityUser.role != "coadmin") || isAdminDivision) &&
|
||||
dataDetail?.isActive && (
|
||||
<BorderBottomItem
|
||||
onPress={() => { router.push(`/division/${id}/add-member`) }}
|
||||
borderType="none"
|
||||
icon={
|
||||
<View style={[Styles.iconContent]}>
|
||||
<Feather name="user-plus" size={25} color={'black'} />
|
||||
</View>
|
||||
}
|
||||
title="Tambah Anggota"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
loading ?
|
||||
arrSkeleton.map((item, index) => {
|
||||
return (
|
||||
<SkeletonTwoItem key={index} />
|
||||
)
|
||||
})
|
||||
:
|
||||
dataMember.map((item, index) => {
|
||||
return (
|
||||
<BorderBottomItem
|
||||
key={index}
|
||||
borderType="bottom"
|
||||
onPress={() => { dataDetail?.isActive && (isAdminDivision || (entityUser.role != "user" && entityUser.role != "coadmin")) && handleChooseMember(item) }}
|
||||
icon={
|
||||
<ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} size="sm" />
|
||||
}
|
||||
title={item.name}
|
||||
rightTopInfo={item.isAdmin ? "Admin" : "Anggota"}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
<DrawerBottom animation="slide" isVisible={isModal} setVisible={setModal} title={dataMemberChoose.name}>
|
||||
<View>
|
||||
<Pressable style={[Styles.wrapItemBorderBottom]} onPress={() => { handleMemberAdmin() }}>
|
||||
<View style={[Styles.rowItemsCenter]}>
|
||||
<View style={[Styles.iconContent]}>
|
||||
<MaterialIcons name="verified-user" size={25} color={'black'} />
|
||||
</View>
|
||||
<View style={[Styles.rowSpaceBetween, { width: '88%' }]}>
|
||||
<View style={[Styles.ml10]}>
|
||||
<Text style={[Styles.textDefault]}>{dataMemberChoose.isAdmin ? 'Memberhentikan sebagai admin' : 'Jadikan admin'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Pressable>
|
||||
|
||||
<Pressable style={[Styles.wrapItemBorderBottom]} onPress={() => { handleMemberOut() }}>
|
||||
<View style={[Styles.rowItemsCenter]}>
|
||||
<View style={[Styles.iconContent, ColorsStatus.info]}>
|
||||
<MaterialCommunityIcons name="close-circle" size={25} color={colors.primary} />
|
||||
</View>
|
||||
<View style={[Styles.rowSpaceBetween, { width: '88%' }]}>
|
||||
<View style={[Styles.ml10]}>
|
||||
<Text style={[Styles.textDefault]}>Keluarkan dari divisi</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Pressable>
|
||||
<View style={Styles.rowItemsCenter}>
|
||||
<MenuItemRow
|
||||
icon={<MaterialIcons name="verified-user" color={colors.text} size={25} />}
|
||||
title={dataMemberChoose.isAdmin ? 'Berhentikan admin' : 'Jadikan admin'}
|
||||
onPress={handleMemberAdmin}
|
||||
/>
|
||||
<MenuItemRow
|
||||
icon={<MaterialCommunityIcons name="account-remove" color={colors.text} size={25} />}
|
||||
title="Keluarkan"
|
||||
onPress={handleMemberOut}
|
||||
/>
|
||||
</View>
|
||||
</DrawerBottom>
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ import Styles from "@/constants/Styles";
|
||||
import { apiGetUser } from "@/lib/api";
|
||||
import { setFormCreateDivision } from "@/lib/divisionCreate";
|
||||
import { useAuthSession } from "@/providers/AuthProvider";
|
||||
import { AntDesign } from "@expo/vector-icons";
|
||||
import { useTheme } from "@/providers/ThemeProvider";
|
||||
import { AntDesign } from "@expo/vector-icons";
|
||||
import { router, Stack, useLocalSearchParams } from "expo-router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Pressable, ScrollView, View } from "react-native";
|
||||
@@ -55,6 +55,21 @@ export default function CreateDivisionAddMember() {
|
||||
}
|
||||
}
|
||||
|
||||
const isAllSelected = data.length > 0 && data.every((item: any) =>
|
||||
selectMember.some((s: any) => s.idUser == item.id)
|
||||
)
|
||||
|
||||
function handleSelectAll() {
|
||||
if (isAllSelected) {
|
||||
setSelectMember([])
|
||||
} else {
|
||||
const newMembers = data
|
||||
.filter((item: any) => !selectMember.some((s: any) => s.idUser == item.id))
|
||||
.map((item: any) => ({ idUser: item.id, name: item.name, img: item.img }))
|
||||
setSelectMember([...selectMember, ...newMembers])
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAddMember() {
|
||||
dispatch(setFormCreateDivision({ ...update, member: selectMember }))
|
||||
router.push(`./add-admin-division`)
|
||||
@@ -93,7 +108,7 @@ export default function CreateDivisionAddMember() {
|
||||
selectMember.length > 0
|
||||
?
|
||||
<View>
|
||||
<ScrollView horizontal style={[Styles.mb10, Styles.pv10]} showsHorizontalScrollIndicator={false}>
|
||||
<ScrollView horizontal style={[Styles.mb05, Styles.pv10]} showsHorizontalScrollIndicator={false}>
|
||||
{
|
||||
selectMember.map((item: any, index: any) => (
|
||||
<ImageWithLabel
|
||||
@@ -114,6 +129,15 @@ export default function CreateDivisionAddMember() {
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
|
||||
{data.length > 0 && (
|
||||
<Pressable
|
||||
style={[Styles.itemSelectModal, { borderColor: colors.icon + '20', paddingTop: 0 }]}
|
||||
onPress={handleSelectAll}
|
||||
>
|
||||
<Text style={[Styles.textMediumSemiBold]}>{isAllSelected ? 'Batalkan Semua' : 'Pilih Semua'}</Text>
|
||||
{isAllSelected && <AntDesign name="check" size={20} color={colors.text} />}
|
||||
</Pressable>
|
||||
)}
|
||||
{
|
||||
data.length > 0 ?
|
||||
data.map((item: any, index: any) => {
|
||||
|
||||
@@ -84,7 +84,7 @@ export default function Notification() {
|
||||
}
|
||||
const parseDate = (str: string) => {
|
||||
const [d, m, y] = str.split(' ')
|
||||
return new Date(Number(y), BULAN[m] ?? 0, Number(d)).getTime()
|
||||
return new Date(Number(y), BULAN[m?.toUpperCase()] ?? 0, Number(d)).getTime()
|
||||
}
|
||||
|
||||
const groups: Record<string, Props[]> = {}
|
||||
|
||||
@@ -78,6 +78,22 @@ export default function AddMemberProject() {
|
||||
}
|
||||
}
|
||||
|
||||
const availableData = data.filter((item: any) => !dataOld.some((i: any) => i.idUser == item.id))
|
||||
const isAllSelected = availableData.length > 0 && availableData.every((item: any) =>
|
||||
selectMember.some((s: any) => s.idUser == item.id)
|
||||
)
|
||||
|
||||
function handleSelectAll() {
|
||||
if (isAllSelected) {
|
||||
setSelectMember([])
|
||||
} else {
|
||||
const newMembers = availableData
|
||||
.filter((item: any) => !selectMember.some((s: any) => s.idUser == item.id))
|
||||
.map((item: any) => ({ idUser: item.id, name: item.name, img: item.img }))
|
||||
setSelectMember([...selectMember, ...newMembers])
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAddMember() {
|
||||
try {
|
||||
setLoading(true)
|
||||
@@ -160,6 +176,15 @@ export default function AddMemberProject() {
|
||||
showsVerticalScrollIndicator={false}
|
||||
style={[Styles.h100, { backgroundColor: colors.background }]}
|
||||
>
|
||||
{availableData.length > 0 && (
|
||||
<Pressable
|
||||
style={[Styles.itemSelectModal, { borderColor: colors.icon + '20', paddingTop: 0 }]}
|
||||
onPress={handleSelectAll}
|
||||
>
|
||||
<Text style={[Styles.textMediumSemiBold]}>{isAllSelected ? 'Batalkan Semua' : 'Pilih Semua'}</Text>
|
||||
{isAllSelected && <AntDesign name="check" size={20} color={colors.text} />}
|
||||
</Pressable>
|
||||
)}
|
||||
{
|
||||
data.length > 0 ?
|
||||
<View style={[Styles.mb100]}>
|
||||
|
||||
@@ -61,6 +61,21 @@ export default function AddMemberCreateProject() {
|
||||
}
|
||||
}
|
||||
|
||||
const isAllSelected = data.length > 0 && data.every((item: any) =>
|
||||
selectMember.some((s: any) => s.idUser == item.id)
|
||||
)
|
||||
|
||||
function handleSelectAll() {
|
||||
if (isAllSelected) {
|
||||
setSelectMember([])
|
||||
} else {
|
||||
const newMembers = data
|
||||
.filter((item: any) => !selectMember.some((s: any) => s.idUser == item.id))
|
||||
.map((item: any) => ({ idUser: item.id, name: item.name, img: item.img }))
|
||||
setSelectMember([...selectMember, ...newMembers])
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAddMember() {
|
||||
try {
|
||||
dispatch(setMemberChoose(selectMember))
|
||||
@@ -134,6 +149,15 @@ export default function AddMemberCreateProject() {
|
||||
style={[Styles.h100, Styles.flex1, { backgroundColor: colors.background }]}
|
||||
>
|
||||
|
||||
{data.length > 0 && (
|
||||
<Pressable
|
||||
style={[Styles.itemSelectModal, { borderColor: colors.icon + '20', paddingTop: 0 }]}
|
||||
onPress={handleSelectAll}
|
||||
>
|
||||
<Text style={[Styles.textMediumSemiBold]}>{isAllSelected ? 'Batalkan Semua' : 'Pilih Semua'}</Text>
|
||||
{isAllSelected && <AntDesign name="check" size={20} color={colors.text} />}
|
||||
</Pressable>
|
||||
)}
|
||||
{
|
||||
data.length > 0 ?
|
||||
data.map((item: any, index: any) => {
|
||||
|
||||
171
components/discussion_general/discussionCommentInput.tsx
Normal file
171
components/discussion_general/discussionCommentInput.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
import Text from "@/components/Text";
|
||||
import { regexOnlySpacesOrEnter } from "@/constants/OnlySpaceOrEnter";
|
||||
import Styles from "@/constants/Styles";
|
||||
import { useTheme } from "@/providers/ThemeProvider";
|
||||
import { Feather, Ionicons, MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons";
|
||||
import * as DocumentPicker from "expo-document-picker";
|
||||
import { Platform, Pressable, ScrollView, TextInput, View } from "react-native";
|
||||
import Toast from "react-native-toast-message";
|
||||
|
||||
type Props = {
|
||||
mode: 'new' | 'edit' | 'locked'
|
||||
lockedReason?: string
|
||||
value: string
|
||||
onChange: (val: string) => void
|
||||
loading: boolean
|
||||
onSend: () => void
|
||||
onCancelEdit?: () => void
|
||||
files?: { uri: string; name: string }[]
|
||||
onAddFile?: (files: { uri: string; name: string }[]) => void
|
||||
onRemoveFile?: (index: number) => void
|
||||
existingFiles?: { id: string; name: string; extension: string }[]
|
||||
onRemoveExistingFile?: (id: string) => void
|
||||
canSend: boolean
|
||||
}
|
||||
|
||||
export default function DiscussionCommentInput({
|
||||
mode, lockedReason, value, onChange, loading, onSend,
|
||||
onCancelEdit, files = [], onAddFile, onRemoveFile,
|
||||
existingFiles = [], onRemoveExistingFile, canSend
|
||||
}: Props) {
|
||||
const { colors } = useTheme()
|
||||
|
||||
async function pickFiles() {
|
||||
const result = await DocumentPicker.getDocumentAsync({ type: ['*/*'], multiple: true })
|
||||
if (!result.canceled && onAddFile) {
|
||||
let skipped = 0
|
||||
const newFiles: { uri: string; name: string }[] = []
|
||||
for (const asset of result.assets) {
|
||||
if (!asset.uri) continue
|
||||
if (files.some(f => f.name === asset.name)) { skipped++; continue }
|
||||
newFiles.push({ uri: asset.uri, name: asset.name })
|
||||
}
|
||||
if (skipped > 0) Toast.show({ type: 'small', text1: 'Beberapa file sudah ditambahkan' })
|
||||
if (newFiles.length > 0) onAddFile(newFiles)
|
||||
}
|
||||
}
|
||||
|
||||
if (mode === 'locked') {
|
||||
return (
|
||||
<View style={[Styles.pv20, Styles.itemsCenter]}>
|
||||
<Text style={[Styles.textInformation, { color: colors.dimmed }]}>{lockedReason}</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const sendDisabled = loading || value.trim() === '' || regexOnlySpacesOrEnter.test(value) || !canSend
|
||||
|
||||
return (
|
||||
<View style={{ backgroundColor: colors.background, borderTopWidth: mode === 'edit' ? 1 : 0, borderTopColor: colors.icon + '20' }}>
|
||||
{mode === 'edit' && (
|
||||
<View style={[Styles.w90, Styles.rowSpaceBetween, Styles.pv05]}>
|
||||
<View style={Styles.rowItemsCenter}>
|
||||
<Feather name="edit-3" color={colors.text} size={20} style={Styles.mh05} />
|
||||
<Text style={Styles.textMediumSemiBold}>Edit Komentar</Text>
|
||||
</View>
|
||||
<Pressable onPress={onCancelEdit}>
|
||||
<MaterialIcons name="close" color={colors.text} size={20} />
|
||||
</Pressable>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{(existingFiles.length > 0 || files.length > 0) && (
|
||||
<ScrollView horizontal style={[Styles.ph15, Styles.pv05]} showsHorizontalScrollIndicator={false}>
|
||||
{existingFiles.map((f) => (
|
||||
<Pressable
|
||||
key={f.id}
|
||||
onPress={() => onRemoveExistingFile?.(f.id)}
|
||||
style={{
|
||||
flexDirection: 'row', alignItems: 'center', gap: 6,
|
||||
paddingHorizontal: 10, paddingVertical: 6,
|
||||
borderRadius: 20, borderWidth: 1,
|
||||
backgroundColor: colors.card, borderColor: colors.icon + '18',
|
||||
marginRight: 8
|
||||
}}
|
||||
>
|
||||
<MaterialCommunityIcons name="file-outline" size={14} color={colors.dimmed} />
|
||||
<Text style={[Styles.textSmallSemiBold, { color: colors.dimmed, maxWidth: 100 }]} numberOfLines={1}>
|
||||
{f.name}.{f.extension}
|
||||
</Text>
|
||||
<MaterialIcons name="close" size={14} color='#F03E3E' />
|
||||
</Pressable>
|
||||
))}
|
||||
{files.map((f, idx) => (
|
||||
<Pressable
|
||||
key={idx}
|
||||
onPress={() => onRemoveFile?.(idx)}
|
||||
style={{
|
||||
flexDirection: 'row', alignItems: 'center', gap: 6,
|
||||
paddingHorizontal: 10, paddingVertical: 6,
|
||||
borderRadius: 20, borderWidth: 1,
|
||||
backgroundColor: colors.card, borderColor: colors.icon + '18',
|
||||
marginRight: 8
|
||||
}}
|
||||
>
|
||||
<MaterialCommunityIcons name="file-outline" size={14} color={colors.dimmed} />
|
||||
<Text style={[Styles.textSmallSemiBold, { color: colors.dimmed, maxWidth: 100 }]} numberOfLines={1}>
|
||||
{f.name}
|
||||
</Text>
|
||||
<MaterialIcons name="close" size={14} color={colors.dimmed} />
|
||||
</Pressable>
|
||||
))}
|
||||
</ScrollView>
|
||||
)}
|
||||
|
||||
<View style={{
|
||||
flexDirection: 'row', alignItems: 'flex-end',
|
||||
paddingHorizontal: 12,
|
||||
paddingBottom: Platform.OS === 'ios' ? 10 : 6,
|
||||
paddingTop: 6,
|
||||
gap: 8
|
||||
}}>
|
||||
{mode === 'new' && (
|
||||
<Pressable
|
||||
onPress={pickFiles}
|
||||
style={{ marginBottom: 6 }}
|
||||
>
|
||||
<MaterialIcons
|
||||
name="add"
|
||||
size={28}
|
||||
color={files.length > 0 ? colors.tabActive : colors.dimmed}
|
||||
/>
|
||||
</Pressable>
|
||||
)}
|
||||
|
||||
<View style={{
|
||||
flex: 1,
|
||||
backgroundColor: colors.input,
|
||||
borderRadius: 30, borderWidth: 1, borderColor: colors.icon + '20',
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 10,
|
||||
}}>
|
||||
<TextInput
|
||||
style={{
|
||||
color: colors.text,
|
||||
maxHeight: 100,
|
||||
paddingVertical: Platform.OS === 'android' ? 4 : 0,
|
||||
textAlignVertical: 'bottom',
|
||||
}}
|
||||
placeholder="Kirim Komentar"
|
||||
placeholderTextColor={colors.dimmed}
|
||||
value={value}
|
||||
onChangeText={onChange}
|
||||
multiline
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Pressable
|
||||
onPress={() => !sendDisabled && onSend()}
|
||||
style={{
|
||||
width: 40, height: 40, borderRadius: 20,
|
||||
backgroundColor: sendDisabled ? colors.dimmed + '40' : colors.tint,
|
||||
justifyContent: 'center', alignItems: 'center',
|
||||
marginBottom: 0
|
||||
}}
|
||||
>
|
||||
<Ionicons name="send" size={18} color={sendDisabled ? colors.dimmed : '#fff'} />
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
275
components/discussion_general/discussionCommentList.tsx
Normal file
275
components/discussion_general/discussionCommentList.tsx
Normal file
@@ -0,0 +1,275 @@
|
||||
import ImageUser from "@/components/imageNew";
|
||||
import Skeleton from "@/components/skeleton";
|
||||
import Text from "@/components/Text";
|
||||
import { ConstEnv } from "@/constants/ConstEnv";
|
||||
import { isImageFile } from "@/constants/FileExtensions";
|
||||
import Styles from "@/constants/Styles";
|
||||
import { useTheme } from "@/providers/ThemeProvider";
|
||||
import { MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons";
|
||||
import * as FileSystem from 'expo-file-system';
|
||||
import { startActivityAsync } from 'expo-intent-launcher';
|
||||
import * as Sharing from 'expo-sharing';
|
||||
import { useState } from "react";
|
||||
import { Modal, Platform, Pressable, SafeAreaView, ScrollView, View } from "react-native";
|
||||
import ImageViewing from "react-native-image-viewing";
|
||||
import * as mime from 'react-native-mime-types';
|
||||
import Toast from "react-native-toast-message";
|
||||
|
||||
export type CommentFile = {
|
||||
id: string
|
||||
name: string
|
||||
extension: string
|
||||
idStorage: string
|
||||
}
|
||||
|
||||
export type CommentItem = {
|
||||
id: string
|
||||
comment: string
|
||||
createdAt: string
|
||||
idUser: string
|
||||
img: string
|
||||
username: string
|
||||
isEdited: boolean
|
||||
updatedAt: string
|
||||
files: CommentFile[]
|
||||
}
|
||||
|
||||
type Props = {
|
||||
data: CommentItem[]
|
||||
loading: boolean
|
||||
myId: string
|
||||
canInteract: boolean
|
||||
onLongPress: (id: string, comment: string, files: CommentFile[]) => void
|
||||
}
|
||||
|
||||
function getFileIcon(ext: string): keyof typeof MaterialCommunityIcons.glyphMap {
|
||||
if (isImageFile(ext)) return 'image-outline'
|
||||
if (ext === 'pdf') return 'file-pdf-box'
|
||||
if (['mp4', 'mov', 'avi', 'mkv'].includes(ext)) return 'video-outline'
|
||||
if (['doc', 'docx'].includes(ext)) return 'file-word-outline'
|
||||
if (['xls', 'xlsx'].includes(ext)) return 'file-excel-outline'
|
||||
if (['zip', 'rar', '7z'].includes(ext)) return 'zip-box-outline'
|
||||
return 'file-outline'
|
||||
}
|
||||
|
||||
function getFileColor(ext: string): string {
|
||||
if (isImageFile(ext)) return '#339AF0'
|
||||
if (ext === 'pdf') return '#F03E3E'
|
||||
if (['mp4', 'mov', 'avi', 'mkv'].includes(ext)) return '#AE3EC9'
|
||||
if (['doc', 'docx'].includes(ext)) return '#1C7ED6'
|
||||
if (['xls', 'xlsx'].includes(ext)) return '#2F9E44'
|
||||
if (['zip', 'rar', '7z'].includes(ext)) return '#E8590C'
|
||||
return '#868E96'
|
||||
}
|
||||
|
||||
function FileCard({ file, colors, onPress }: { file: CommentFile; colors: any; onPress: () => void }) {
|
||||
const ext = file.extension.toLowerCase()
|
||||
return (
|
||||
<Pressable
|
||||
onPress={onPress}
|
||||
style={({ pressed }) => [Styles.fileCard, {
|
||||
borderColor: colors.icon + '18',
|
||||
backgroundColor: pressed ? colors.icon + '10' : 'transparent'
|
||||
}]}
|
||||
>
|
||||
<View style={[Styles.sectionIconBox, { backgroundColor: getFileColor(ext) + '20' }]}>
|
||||
<MaterialCommunityIcons name={getFileIcon(ext)} size={18} color={getFileColor(ext)} />
|
||||
</View>
|
||||
<View style={Styles.flex1}>
|
||||
<Text style={Styles.textDefault} numberOfLines={1}>{file.name}</Text>
|
||||
<Text style={[Styles.textSmallSemiBold, { color: colors.dimmed }]}>{ext.toUpperCase()}</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
||||
export default function DiscussionCommentList({ data, loading, myId, canInteract, onLongPress }: Props) {
|
||||
const { colors } = useTheme()
|
||||
const [expandedIds, setExpandedIds] = useState<string[]>([])
|
||||
const [modalFiles, setModalFiles] = useState<CommentFile[]>([])
|
||||
const [modalVisible, setModalVisible] = useState(false)
|
||||
const [previewFile, setPreviewFile] = useState<CommentFile | null>(null)
|
||||
const [modalPreviewFile, setModalPreviewFile] = useState<CommentFile | null>(null)
|
||||
const [loadingOpen, setLoadingOpen] = useState(false)
|
||||
const arrSkeleton = Array.from({ length: 3 }, (_, i) => i)
|
||||
|
||||
function toggleExpand(id: string) {
|
||||
setExpandedIds(prev => prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id])
|
||||
}
|
||||
|
||||
async function openExternal(file: CommentFile) {
|
||||
try {
|
||||
setLoadingOpen(true)
|
||||
const remoteUrl = `${ConstEnv.url_storage}/files/${file.idStorage}`
|
||||
const fileName = `${file.name}.${file.extension}`
|
||||
const localPath = `${FileSystem.documentDirectory}/${fileName}`
|
||||
const dl = await FileSystem.downloadAsync(remoteUrl, localPath)
|
||||
if (dl.status !== 200) throw new Error('Download failed')
|
||||
const contentURL = await FileSystem.getContentUriAsync(dl.uri)
|
||||
const mimeType = mime.lookup(fileName) as string
|
||||
if (Platform.OS === 'android') {
|
||||
await startActivityAsync('android.intent.action.VIEW', { data: contentURL, flags: 1, type: mimeType })
|
||||
} else {
|
||||
await Sharing.shareAsync(localPath)
|
||||
}
|
||||
} catch {
|
||||
Toast.show({ type: 'error', text1: 'Gagal membuka file' })
|
||||
} finally {
|
||||
setLoadingOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
function handleFilePress(file: CommentFile) {
|
||||
if (isImageFile(file.extension.toLowerCase())) {
|
||||
setPreviewFile(file)
|
||||
} else {
|
||||
openExternal(file)
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<View style={Styles.mt10}>
|
||||
{arrSkeleton.map((_, i) => (
|
||||
<Skeleton key={i} width={100} widthType="percent" height={40} borderRadius={5} />
|
||||
))}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style={Styles.mt10}>
|
||||
{data.map((item, i) => (
|
||||
<Pressable
|
||||
key={i}
|
||||
onPress={() => toggleExpand(item.id)}
|
||||
onLongPress={() => item.idUser === myId && canInteract && onLongPress(item.id, item.comment, item.files ?? [])}
|
||||
style={({ pressed }) => [
|
||||
Styles.discussionCommentCard,
|
||||
{ backgroundColor: pressed ? colors.icon + '10' : colors.card, borderColor: colors.icon + '20' }
|
||||
]}
|
||||
>
|
||||
<View style={Styles.flex1}>
|
||||
<View style={[Styles.rowSpaceBetween, Styles.itemsCenter, Styles.mb05]}>
|
||||
<View style={[Styles.rowItemsCenter, { gap: 8, flex: 1, marginRight: 8 }]}>
|
||||
<ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} size="xs" />
|
||||
<Text style={[Styles.textMediumSemiBold, { color: colors.text }]} numberOfLines={1}>
|
||||
{item.username}
|
||||
</Text>
|
||||
{item.isEdited && (
|
||||
<Text style={[Styles.discussionEditedText, { color: colors.dimmed }]}>diedit</Text>
|
||||
)}
|
||||
</View>
|
||||
<Text style={[Styles.discussionDateText, { color: colors.dimmed, flexShrink: 0 }]}>
|
||||
{item.createdAt}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{item.comment.length > 0 && (
|
||||
<Text
|
||||
style={[Styles.textDefault, { color: colors.text }]}
|
||||
numberOfLines={expandedIds.includes(item.id) ? 0 : 3}
|
||||
>
|
||||
{item.comment}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{item.files?.length > 0 && (
|
||||
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 6, marginTop: item.comment.length > 0 ? 8 : 0 }}>
|
||||
{(item.files.length > 2 ? item.files.slice(0, 1) : item.files).map((file, idx) => (
|
||||
<FileCard key={idx} file={file} colors={colors} onPress={() => handleFilePress(file)} />
|
||||
))}
|
||||
{item.files.length > 2 && (
|
||||
<Pressable
|
||||
onPress={() => { setModalFiles(item.files); setModalVisible(true) }}
|
||||
style={[Styles.fileCard, { borderColor: colors.icon + '18', backgroundColor: 'transparent' }]}
|
||||
>
|
||||
<View style={[Styles.sectionIconBox, { backgroundColor: '#868E96' + '20' }]}>
|
||||
<MaterialCommunityIcons name="folder-multiple-outline" size={18} color="#868E96" />
|
||||
</View>
|
||||
<View style={Styles.flex1}>
|
||||
<Text style={Styles.textDefault} numberOfLines={1}>+{item.files.length - 1} lainnya</Text>
|
||||
<Text style={[Styles.textSmallSemiBold, { color: colors.dimmed }]}>Lihat semua</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<Modal visible={modalVisible} animationType="slide" onRequestClose={() => setModalVisible(false)}>
|
||||
<SafeAreaView style={{ flex: 1, backgroundColor: colors.background }}>
|
||||
<View style={[Styles.rowSpaceBetween, Styles.itemsCenter, Styles.ph15, Styles.pv10, { borderBottomWidth: 1, borderBottomColor: colors.icon + '20' }]}>
|
||||
<Text style={Styles.textLargeSemiBold}>Lampiran ({modalFiles.length} file)</Text>
|
||||
<Pressable onPress={() => setModalVisible(false)}>
|
||||
<MaterialIcons name="close" size={24} color={colors.text} />
|
||||
</Pressable>
|
||||
</View>
|
||||
<ScrollView contentContainerStyle={[Styles.ph15, Styles.pv10, { flexDirection: 'row', flexWrap: 'wrap', gap: 8 }]}>
|
||||
{modalFiles.map((file, idx) => (
|
||||
<FileCard
|
||||
key={idx} file={file} colors={colors}
|
||||
onPress={() => {
|
||||
if (isImageFile(file.extension.toLowerCase())) {
|
||||
setModalPreviewFile(file)
|
||||
} else {
|
||||
openExternal(file)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
<ImageViewing
|
||||
images={[{ uri: `${ConstEnv.url_storage}/files/${modalPreviewFile?.idStorage}` }]}
|
||||
imageIndex={0}
|
||||
visible={modalPreviewFile !== null}
|
||||
onRequestClose={() => setModalPreviewFile(null)}
|
||||
doubleTapToZoomEnabled
|
||||
HeaderComponent={() => (
|
||||
<View style={Styles.headerModalViewImg}>
|
||||
<Pressable onPress={() => setModalPreviewFile(null)}>
|
||||
<Text style={{ color: 'white', fontSize: 26 }}>✕</Text>
|
||||
</Pressable>
|
||||
<Pressable onPress={() => modalPreviewFile && openExternal(modalPreviewFile)} disabled={loadingOpen}>
|
||||
<Text style={{ color: loadingOpen ? 'gray' : 'white', fontSize: 26 }}>⋯</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
)}
|
||||
FooterComponent={() => (
|
||||
<View style={{ paddingBottom: 20, paddingHorizontal: 16, alignItems: 'center' }}>
|
||||
<Text style={{ color: 'white', fontSize: 16 }}>{modalPreviewFile?.name}.{modalPreviewFile?.extension}</Text>
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<ImageViewing
|
||||
images={[{ uri: `${ConstEnv.url_storage}/files/${previewFile?.idStorage}` }]}
|
||||
imageIndex={0}
|
||||
visible={previewFile !== null}
|
||||
onRequestClose={() => setPreviewFile(null)}
|
||||
doubleTapToZoomEnabled
|
||||
HeaderComponent={() => (
|
||||
<View style={Styles.headerModalViewImg}>
|
||||
<Pressable onPress={() => setPreviewFile(null)}>
|
||||
<Text style={{ color: 'white', fontSize: 26 }}>✕</Text>
|
||||
</Pressable>
|
||||
<Pressable onPress={() => previewFile && openExternal(previewFile)} disabled={loadingOpen}>
|
||||
<Text style={{ color: loadingOpen ? 'gray' : 'white', fontSize: 26 }}>⋯</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
)}
|
||||
FooterComponent={() => (
|
||||
<View style={{ paddingBottom: 20, paddingHorizontal: 16, alignItems: 'center' }}>
|
||||
<Text style={{ color: 'white', fontSize: 16 }}>{previewFile?.name}.{previewFile?.extension}</Text>
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
287
components/discussion_general/modalSelectMemberByDivision.tsx
Normal file
287
components/discussion_general/modalSelectMemberByDivision.tsx
Normal file
@@ -0,0 +1,287 @@
|
||||
import { ButtonForm } from "@/components/buttonForm"
|
||||
import DrawerBottom from "@/components/drawerBottom"
|
||||
import ImageUser from "@/components/imageNew"
|
||||
import ImageWithLabel from "@/components/imageWithLabel"
|
||||
import InputSearch from "@/components/inputSearch"
|
||||
import Text from "@/components/Text"
|
||||
import { ConstEnv } from "@/constants/ConstEnv"
|
||||
import Styles from "@/constants/Styles"
|
||||
import { apiGetDivision, apiGetDivisionMember, apiGetUser } from "@/lib/api"
|
||||
import { setMemberChoose } from "@/lib/memberChoose"
|
||||
import { useAuthSession } from "@/providers/AuthProvider"
|
||||
import { useTheme } from "@/providers/ThemeProvider"
|
||||
import { AntDesign, Ionicons } from "@expo/vector-icons"
|
||||
import { useEffect, useState } from "react"
|
||||
import { ActivityIndicator, Pressable, ScrollView, View } from "react-native"
|
||||
import { useDispatch, useSelector } from "react-redux"
|
||||
|
||||
type Member = { idUser: string; name: string; img: string }
|
||||
|
||||
type DivisionItem = {
|
||||
id: string
|
||||
name: string
|
||||
expanded: boolean
|
||||
membersLoaded: boolean
|
||||
members: Member[]
|
||||
}
|
||||
|
||||
type Props = {
|
||||
open: boolean
|
||||
close: (value: boolean) => void
|
||||
idGroup: string
|
||||
}
|
||||
|
||||
export default function ModalSelectMemberByDivision({ open, close, idGroup }: Props) {
|
||||
const { token, decryptToken } = useAuthSession()
|
||||
const { colors } = useTheme()
|
||||
const dispatch = useDispatch()
|
||||
const entitiesMember = useSelector((state: any) => state.memberChoose)
|
||||
|
||||
const [divisions, setDivisions] = useState<DivisionItem[]>([])
|
||||
const [selectMember, setSelectMember] = useState<Member[]>([])
|
||||
const [search, setSearch] = useState('')
|
||||
const [loadingDivisions, setLoadingDivisions] = useState(false)
|
||||
const [loadingIds, setLoadingIds] = useState<string[]>([])
|
||||
const [searchResults, setSearchResults] = useState<any[]>([])
|
||||
const [loadingSearch, setLoadingSearch] = useState(false)
|
||||
|
||||
async function loadDivisions() {
|
||||
if (!idGroup) return
|
||||
setLoadingDivisions(true)
|
||||
try {
|
||||
const hasil = await decryptToken(String(token?.current))
|
||||
const response = await apiGetDivision({ user: hasil, search: '', group: idGroup, active: 'true', kategori: 'semua', page: 1 })
|
||||
const divisionList: DivisionItem[] = (response.data ?? []).map((d: any) => ({
|
||||
id: d.id, name: d.name, expanded: false, membersLoaded: false, members: []
|
||||
}))
|
||||
const withMembers = await Promise.all(
|
||||
divisionList.map(async (d) => {
|
||||
try {
|
||||
const res = await apiGetDivisionMember({ user: hasil, id: d.id, search: '' })
|
||||
const members: Member[] = (res.data ?? []).map((m: any) => ({ idUser: m.idUser, name: m.name, img: m.img }))
|
||||
return { ...d, members, membersLoaded: true }
|
||||
} catch {
|
||||
return { ...d, membersLoaded: true }
|
||||
}
|
||||
})
|
||||
)
|
||||
setDivisions(withMembers)
|
||||
} catch { setDivisions([]) }
|
||||
finally { setLoadingDivisions(false) }
|
||||
}
|
||||
|
||||
async function fetchMembers(divisionId: string): Promise<Member[]> {
|
||||
setLoadingIds(prev => [...prev, divisionId])
|
||||
try {
|
||||
const hasil = await decryptToken(String(token?.current))
|
||||
const response = await apiGetDivisionMember({ user: hasil, id: divisionId, search: '' })
|
||||
const members: Member[] = (response.data ?? []).map((m: any) => ({ idUser: m.idUser, name: m.name, img: m.img }))
|
||||
setDivisions(prev => prev.map(d =>
|
||||
d.id === divisionId ? { ...d, members, membersLoaded: true } : d
|
||||
))
|
||||
return members
|
||||
} catch { return [] }
|
||||
finally { setLoadingIds(prev => prev.filter(id => id !== divisionId)) }
|
||||
}
|
||||
|
||||
async function searchUsers(query: string) {
|
||||
setLoadingSearch(true)
|
||||
try {
|
||||
const hasil = await decryptToken(String(token?.current))
|
||||
const response = await apiGetUser({ user: hasil, active: 'true', search: query, group: idGroup })
|
||||
setSearchResults((response.data ?? []).filter((i: any) => i.idUserRole !== 'supadmin'))
|
||||
} catch { setSearchResults([]) }
|
||||
finally { setLoadingSearch(false) }
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (open) { loadDivisions(); setSelectMember(entitiesMember) }
|
||||
}, [open])
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
if (search) {
|
||||
searchUsers(search)
|
||||
} else {
|
||||
setSearchResults([])
|
||||
loadDivisions()
|
||||
}
|
||||
}, [search])
|
||||
|
||||
async function handleTapDivision(division: DivisionItem) {
|
||||
let members = division.members
|
||||
if (!division.membersLoaded) members = await fetchMembers(division.id)
|
||||
setDivisions(prev => prev.map(d =>
|
||||
d.id === division.id ? { ...d, expanded: true, members, membersLoaded: true } : d
|
||||
))
|
||||
const allSelected = members.length > 0 && members.every(m =>
|
||||
selectMember.some(s => s.idUser === m.idUser)
|
||||
)
|
||||
if (allSelected) {
|
||||
setSelectMember(prev => prev.filter(s => !members.some(m => m.idUser === s.idUser)))
|
||||
} else {
|
||||
const existingIds = new Set(selectMember.map(s => s.idUser))
|
||||
setSelectMember(prev => [...prev, ...members.filter(m => !existingIds.has(m.idUser))])
|
||||
}
|
||||
}
|
||||
|
||||
async function handleToggleExpand(divisionId: string) {
|
||||
const division = divisions.find(d => d.id === divisionId)!
|
||||
if (!division.membersLoaded && !division.expanded) await fetchMembers(divisionId)
|
||||
setDivisions(prev => prev.map(d =>
|
||||
d.id === divisionId ? { ...d, expanded: !d.expanded } : d
|
||||
))
|
||||
}
|
||||
|
||||
function handleToggleMember(member: Member) {
|
||||
if (selectMember.some(s => s.idUser === member.idUser)) {
|
||||
setSelectMember(prev => prev.filter(s => s.idUser !== member.idUser))
|
||||
} else {
|
||||
setSelectMember(prev => [...prev, member])
|
||||
}
|
||||
}
|
||||
|
||||
function handleConfirm() {
|
||||
dispatch(setMemberChoose(selectMember))
|
||||
handleClose()
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
setDivisions([])
|
||||
setSelectMember([])
|
||||
setSearch('')
|
||||
close(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<DrawerBottom animation="none" isVisible={open} setVisible={handleClose} title="Pilih Anggota" height={90}>
|
||||
<InputSearch onChange={setSearch} value={search} bg="transparent" />
|
||||
{selectMember.length > 0
|
||||
? (
|
||||
<View>
|
||||
<ScrollView horizontal style={[Styles.mb10, Styles.pv10]} showsHorizontalScrollIndicator={false}>
|
||||
{selectMember.map((item, index) => (
|
||||
<ImageWithLabel
|
||||
key={index}
|
||||
label={item.name}
|
||||
src={`${ConstEnv.url_storage}/files/${item.img}`}
|
||||
onClick={() => handleToggleMember(item)}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
)
|
||||
: (
|
||||
<Text style={[Styles.textDefault, { color: colors.dimmed, textAlign: 'center' }, Styles.pv05]}>
|
||||
Tidak ada member yang dipilih
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
<ScrollView showsVerticalScrollIndicator={false}>
|
||||
<View>
|
||||
{search ? (
|
||||
loadingSearch ? (
|
||||
<ActivityIndicator color={colors.tabActive} style={{ marginTop: 20 }} />
|
||||
) : searchResults.length > 0 ? (
|
||||
searchResults.map((item, idx) => (
|
||||
<Pressable
|
||||
key={idx}
|
||||
style={[Styles.itemSelectModal, { borderColor: colors.icon + '20' }]}
|
||||
onPress={() => handleToggleMember({ idUser: item.id, name: item.name, img: item.img })}
|
||||
>
|
||||
<View style={Styles.rowItemsCenter}>
|
||||
<ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} border />
|
||||
<Text style={[Styles.textDefault, Styles.ml10]}>{item.name}</Text>
|
||||
</View>
|
||||
{selectMember.some(s => s.idUser === item.id) && (
|
||||
<AntDesign name="check" size={18} color={colors.tabActive} />
|
||||
)}
|
||||
</Pressable>
|
||||
))
|
||||
) : (
|
||||
<Text style={[Styles.textDefault, { textAlign: 'center', color: colors.dimmed, marginTop: 20 }]}>
|
||||
Tidak ada hasil
|
||||
</Text>
|
||||
)
|
||||
) : loadingDivisions ? (
|
||||
<ActivityIndicator color={colors.tabActive} style={{ marginTop: 20 }} />
|
||||
) : divisions.length > 0 ? (
|
||||
divisions.map((division) => {
|
||||
const selectedCount = division.members.filter(m =>
|
||||
selectMember.some(s => s.idUser === m.idUser)
|
||||
).length
|
||||
const allSelected = division.membersLoaded && division.members.length > 0
|
||||
&& selectedCount === division.members.length
|
||||
const someSelected = selectedCount > 0 && !allSelected
|
||||
const isLoadingThis = loadingIds.includes(division.id)
|
||||
|
||||
return (
|
||||
<View key={division.id}>
|
||||
<Pressable
|
||||
style={[Styles.itemSelectModal, { borderColor: colors.icon + '20' }]}
|
||||
onPress={() => handleTapDivision(division)}
|
||||
>
|
||||
<View style={Styles.flex1}>
|
||||
<Text style={[Styles.textDefaultSemiBold, { color: colors.text }]}>{division.name}</Text>
|
||||
{division.membersLoaded && (
|
||||
<Text style={[Styles.textSmallSemiBold, { color: colors.dimmed }]}>
|
||||
{selectedCount > 0
|
||||
? `${selectedCount} dari ${division.members.length} dipilih`
|
||||
: `${division.members.length} anggota`}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
{isLoadingThis ? (
|
||||
<ActivityIndicator size="small" color={colors.dimmed} />
|
||||
) : allSelected ? (
|
||||
<AntDesign name="checkcircle" size={18} color={colors.tabActive} />
|
||||
) : someSelected ? (
|
||||
<AntDesign name="checkcircleo" size={18} color={colors.tabActive} />
|
||||
) : null}
|
||||
<Pressable
|
||||
onPress={() => handleToggleExpand(division.id)}
|
||||
style={{ paddingLeft: 10 }}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
>
|
||||
<Ionicons
|
||||
name={division.expanded ? "chevron-up" : "chevron-down"}
|
||||
size={16}
|
||||
color={colors.dimmed}
|
||||
/>
|
||||
</Pressable>
|
||||
</Pressable>
|
||||
|
||||
{division.expanded && division.members.map((member, idx) => (
|
||||
<Pressable
|
||||
key={idx}
|
||||
style={[Styles.itemSelectModal, { borderColor: colors.icon + '15' }]}
|
||||
onPress={() => handleToggleMember(member)}
|
||||
>
|
||||
<View style={Styles.rowItemsCenter}>
|
||||
<ImageUser src={`${ConstEnv.url_storage}/files/${member.img}`} border />
|
||||
<Text style={[Styles.textDefault, Styles.ml10]}>{member.name}</Text>
|
||||
</View>
|
||||
{selectMember.some(s => s.idUser === member.idUser) && (
|
||||
<AntDesign name="check" size={18} color={colors.tabActive} />
|
||||
)}
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<Text style={[Styles.textDefault, { textAlign: 'center', color: colors.dimmed, marginTop: 20 }]}>
|
||||
Tidak ada divisi
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
<ButtonForm
|
||||
onPress={handleConfirm}
|
||||
text="PILIH MEMBER"
|
||||
disabled={selectMember.length === 0}
|
||||
/>
|
||||
</DrawerBottom>
|
||||
)
|
||||
}
|
||||
@@ -67,9 +67,8 @@ export default function CaraouselHome2({ refreshing }: { refreshing: boolean })
|
||||
<View
|
||||
style={[
|
||||
Styles.mv15,
|
||||
Styles.p15,
|
||||
Styles.round05,
|
||||
{ backgroundColor: colors.card },
|
||||
{ backgroundColor: colors.card, paddingVertical: 15, paddingHorizontal: 10 },
|
||||
Styles.wrapHomeCarousel
|
||||
]}
|
||||
>
|
||||
|
||||
@@ -10,7 +10,7 @@ type Props = {
|
||||
onError?: (val:boolean) => void
|
||||
}
|
||||
|
||||
export default function ImageUser({ src, size, border, onError }: Props) {
|
||||
export default function ImageUser({ src, size, border = true, onError }: Props) {
|
||||
const [error, setError] = useState(false)
|
||||
const { colors } = useTheme()
|
||||
return (
|
||||
|
||||
@@ -97,7 +97,7 @@ export default function ItemSectionTanggalTugas({ status, title, dateStart, date
|
||||
borderWidth: 1,
|
||||
borderColor: colors.icon + '18',
|
||||
backgroundColor: colors.card,
|
||||
marginBottom: 10,
|
||||
marginBottom: 0,
|
||||
}}
|
||||
>
|
||||
{/* Accent bar kiri */}
|
||||
|
||||
@@ -128,6 +128,35 @@ export default function ModalSelectMultiple({ open, close, title, category, choo
|
||||
};
|
||||
|
||||
|
||||
const groupsWithDivisions = data.filter((group: any) => group.Division?.length > 0)
|
||||
const isAllSelected = category === 'choose-division'
|
||||
? groupsWithDivisions.length > 0 && groupsWithDivisions.every((group: any) =>
|
||||
checked[group.id]?.length === group.Division?.length
|
||||
)
|
||||
: data.length > 0 && selectedDivision.length === data.length
|
||||
|
||||
const handleSelectAll = () => {
|
||||
if (category === 'choose-division') {
|
||||
if (isAllSelected) {
|
||||
setChecked({})
|
||||
} else {
|
||||
const newChecked: CheckedState = {}
|
||||
data.forEach((group: any) => {
|
||||
if (group.Division?.length > 0) {
|
||||
newChecked[group.id] = group.Division.map((d: any) => d.id)
|
||||
}
|
||||
})
|
||||
setChecked(newChecked)
|
||||
}
|
||||
} else {
|
||||
if (isAllSelected) {
|
||||
setSelectedDivision([])
|
||||
} else {
|
||||
setSelectedDivision(data.map((d: any) => ({ id: d.id, name: d.name })))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (category == "choose-division") {
|
||||
const selectedGroups: GroupData[] = [];
|
||||
@@ -154,25 +183,32 @@ export default function ModalSelectMultiple({ open, close, title, category, choo
|
||||
{
|
||||
category == 'share-division' ?
|
||||
<>
|
||||
{
|
||||
data.map((item: any, index: number) => {
|
||||
return (
|
||||
<Pressable key={index} style={[Styles.itemSelectModal, { borderColor: colors.icon + 20 }]} onPress={() => {
|
||||
handleDivisionClick(index)
|
||||
}}>
|
||||
<Text numberOfLines={1} style={[Styles.w80]}>{item.name}</Text>
|
||||
{
|
||||
selectedDivision.some((i: any) => i.id == item.id)
|
||||
? <AntDesign name="check" size={17} color={colors.text} />
|
||||
: <></>
|
||||
}
|
||||
</Pressable>
|
||||
)
|
||||
})
|
||||
}
|
||||
<Pressable style={[Styles.itemSelectModal, { borderColor: colors.icon + 20 }]} onPress={handleSelectAll}>
|
||||
<Text style={[Styles.textMediumSemiBold]}>{isAllSelected ? 'Batalkan Semua' : 'Pilih Semua'}</Text>
|
||||
{isAllSelected && <AntDesign name="check" size={17} color={colors.text} />}
|
||||
</Pressable>
|
||||
{data.map((item: any, index: number) => {
|
||||
return (
|
||||
<Pressable key={index} style={[Styles.itemSelectModal, { borderColor: colors.icon + 20 }]} onPress={() => {
|
||||
handleDivisionClick(index)
|
||||
}}>
|
||||
<Text numberOfLines={1} style={[Styles.w80]}>{item.name}</Text>
|
||||
{
|
||||
selectedDivision.some((i: any) => i.id == item.id)
|
||||
? <AntDesign name="check" size={17} color={colors.text} />
|
||||
: <></>
|
||||
}
|
||||
</Pressable>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
:
|
||||
data.map((item: any, index: number) => {
|
||||
<>
|
||||
<Pressable style={[Styles.itemSelectModal, { borderColor: colors.icon + 20 }]} onPress={handleSelectAll}>
|
||||
<Text style={[Styles.textMediumSemiBold]}>{isAllSelected ? 'Batalkan Semua' : 'Pilih Semua'}</Text>
|
||||
{isAllSelected && <AntDesign name="check" size={17} color={colors.text} />}
|
||||
</Pressable>
|
||||
{data.map((item: any, index: number) => {
|
||||
return (
|
||||
<View key={index}>
|
||||
<Pressable style={[Styles.itemSelectModal, { borderColor: colors.icon + 20 }]} onPress={() => { handleGroupCheck(item.id) }}>
|
||||
@@ -199,7 +235,8 @@ export default function ModalSelectMultiple({ open, close, title, category, choo
|
||||
}
|
||||
</View>
|
||||
)
|
||||
})
|
||||
})}
|
||||
</>
|
||||
}
|
||||
</ScrollView>
|
||||
<View style={[Styles.absolute0, { width: '100%' }]}>
|
||||
@@ -207,4 +244,4 @@ export default function ModalSelectMultiple({ open, close, title, category, choo
|
||||
</View>
|
||||
</DrawerBottom>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ export default function SectionFile({ status, member, refreshing }: { status: nu
|
||||
<>
|
||||
<ModalLoading isVisible={loadingOpen} setVisible={setLoadingOpen} />
|
||||
<View style={[Styles.mb15]}>
|
||||
<Text style={[Styles.textDefaultSemiBold, Styles.mv05]}>File</Text>
|
||||
<Text style={[Styles.textDefaultSemiBold, { marginBottom: 5 }]}>File</Text>
|
||||
|
||||
{loading ? (
|
||||
<View style={Styles.fileGrid}>
|
||||
|
||||
@@ -74,7 +74,7 @@ export default function SectionMember({ status, refreshing }: { status: number |
|
||||
return (
|
||||
<>
|
||||
<View style={[Styles.mb15]}>
|
||||
<View style={[Styles.rowSpaceBetween, Styles.mv05]}>
|
||||
<View style={[Styles.rowSpaceBetween, { marginBottom: 5 }]}>
|
||||
<Text style={[Styles.textDefaultSemiBold]}>Anggota</Text>
|
||||
{!loading && data.length > 0 && (
|
||||
<Text style={[Styles.textDefault, { color: colors.dimmed }]}>{data.length} orang</Text>
|
||||
|
||||
@@ -174,9 +174,9 @@ export default function SectionTanggalTugasProject({ status, member, refreshing,
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style={[Styles.mb15, Styles.mt10]}>
|
||||
<Text style={[Styles.textDefaultSemiBold, Styles.mv05]}>Tanggal & Tugas</Text>
|
||||
<View>
|
||||
<View style={[Styles.mb15]}>
|
||||
<Text style={[Styles.textDefaultSemiBold, { marginBottom: 5 }]}>Tanggal & Tugas</Text>
|
||||
<View style={{ gap: 10 }}>
|
||||
{loading
|
||||
? arrSkeleton.map((_, index) => <SkeletonTask key={index} />)
|
||||
: data.length > 0
|
||||
|
||||
@@ -153,7 +153,7 @@ export default function SectionFileTask({ refreshing, isMemberDivision }: { refr
|
||||
<>
|
||||
<ModalLoading isVisible={loadingOpen} setVisible={setLoadingOpen} />
|
||||
<View style={[Styles.mb15]}>
|
||||
<Text style={[Styles.textDefaultSemiBold, Styles.mv05]}>File</Text>
|
||||
<Text style={[Styles.textDefaultSemiBold, { marginBottom: 5 }]}>File</Text>
|
||||
{loading ? (
|
||||
<View style={Styles.fileGrid}>
|
||||
{arrSkeleton.map((_, index) => (
|
||||
|
||||
@@ -100,7 +100,7 @@ export default function SectionMemberTask({ refreshing, isAdminDivision }: { ref
|
||||
return (
|
||||
<>
|
||||
<View style={[Styles.mb15]}>
|
||||
<View style={[Styles.rowSpaceBetween, Styles.mv05]}>
|
||||
<View style={[Styles.rowSpaceBetween, { marginBottom: 5 }]}>
|
||||
<Text style={[Styles.textDefaultSemiBold]}>Anggota</Text>
|
||||
{!loading && data.length > 0 && (
|
||||
<Text style={[Styles.textDefault, { color: colors.dimmed }]}>{data.length} orang</Text>
|
||||
|
||||
@@ -172,9 +172,9 @@ export default function SectionTanggalTugasTask({ refreshing, isMemberDivision,
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style={[Styles.mb15, Styles.mt10]}>
|
||||
<Text style={[Styles.textDefaultSemiBold, Styles.mv05]}>Tanggal & Tugas</Text>
|
||||
<View>
|
||||
<View style={[Styles.mb15]}>
|
||||
<Text style={[Styles.textDefaultSemiBold, { marginBottom: 5 }]}>Tanggal & Tugas</Text>
|
||||
<View style={{ gap: 10 }}>
|
||||
{loading
|
||||
? arrSkeleton.map((_, index) => <SkeletonTask key={index} />)
|
||||
: data.length > 0
|
||||
|
||||
@@ -4,7 +4,7 @@ const AnnouncementStyles = StyleSheet.create({
|
||||
// list (index.tsx)
|
||||
announcementListContainer: { padding: 15, paddingBottom: 0 },
|
||||
announcementListInner: { marginTop: 10 },
|
||||
announcementListCard: { borderRadius: 10, borderWidth: 1, padding: 12 },
|
||||
announcementListCard: { borderRadius: 8, borderWidth: 1, padding: 12 },
|
||||
announcementListCardHeader: { marginBottom: 6 },
|
||||
announcementListTitleRow: { flex: 1, gap: 8, marginRight: 8, flexDirection: 'row', alignItems: 'center' },
|
||||
announcementListIconBox: { width: 28, height: 28, borderRadius: 8, alignItems: 'center', justifyContent: 'center' },
|
||||
@@ -18,11 +18,11 @@ const AnnouncementStyles = StyleSheet.create({
|
||||
|
||||
// detail ([id].tsx)
|
||||
announcementDetailContainer: { padding: 15, paddingBottom: 50, gap: 12 },
|
||||
announcementDetailCard: { borderRadius: 10 },
|
||||
announcementDetailCard: { borderRadius: 8 },
|
||||
announcementDetailSkeletonGap: { gap: 8 },
|
||||
announcementDetailSkeletonIconRow: { gap: 10, marginBottom: 2 },
|
||||
announcementDetailTitleRow: { gap: 10, marginBottom: 10 },
|
||||
announcementDetailIconBox: { width: 38, height: 38, borderRadius: 10 },
|
||||
announcementDetailIconBox: { width: 38, height: 38, borderRadius: 8 },
|
||||
announcementDetailTitleText: { fontSize: 17, lineHeight: 24, flex: 1 },
|
||||
announcementDetailSectionLabelRow: { marginBottom: 8, gap: 6 },
|
||||
announcementDetailFileCardPadding: { padding: 10 },
|
||||
@@ -33,7 +33,7 @@ const AnnouncementStyles = StyleSheet.create({
|
||||
announcementDetailGroupSeparator: { marginTop: 12, paddingTop: 12, borderTopWidth: 1 },
|
||||
announcementDetailGroupLabel: { marginBottom: 6 },
|
||||
announcementDetailDivisionRow: { gap: 8, paddingVertical: 6 },
|
||||
announcementDetailDivisionIconCircle: { width: 26, height: 26, borderRadius: 13, alignItems: 'center', justifyContent: 'center' },
|
||||
announcementDetailDivisionIconCircle: { width: 26, height: 26, borderRadius: 100, alignItems: 'center', justifyContent: 'center' },
|
||||
});
|
||||
|
||||
export default AnnouncementStyles;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { StyleSheet } from "react-native";
|
||||
|
||||
const ApprovalStyles = StyleSheet.create({
|
||||
approvalBadge: { borderRadius: 20, paddingHorizontal: 10, paddingVertical: 3, alignSelf: 'flex-start' },
|
||||
approvalItem: { borderWidth: 1, borderRadius: 10, padding: 12, marginBottom: 10 },
|
||||
approvalItem: { borderWidth: 1, borderRadius: 8, padding: 12, marginBottom: 10 },
|
||||
approvalItemHeader: { justifyContent: 'space-between', marginBottom: 8 },
|
||||
approvalIconMr: { marginRight: 6 },
|
||||
approvalNoteBox: { borderRadius: 8, padding: 8, marginTop: 4 },
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { StyleSheet } from "react-native";
|
||||
|
||||
const BorderStyles = StyleSheet.create({
|
||||
round05: { borderRadius: 5 },
|
||||
round04: { borderRadius: 4 },
|
||||
round05: { borderRadius: 8 },
|
||||
round08: { borderRadius: 8 },
|
||||
round10: { borderRadius: 10 },
|
||||
round12: { borderRadius: 12 },
|
||||
round15: { borderRadius: 15 },
|
||||
round20: { borderRadius: 20 },
|
||||
round30: { borderRadius: 30 },
|
||||
roundFull: { borderRadius: 100 },
|
||||
borderRight: { borderRightWidth: 1, borderRightColor: '#d6d8f6' },
|
||||
borderLeft: { borderLeftWidth: 1, borderLeftColor: '#d6d8f6' },
|
||||
borderBottom: { borderBottomWidth: 1, borderBottomColor: '#d6d8f6' },
|
||||
|
||||
@@ -2,7 +2,7 @@ import { StyleSheet } from "react-native";
|
||||
|
||||
const ButtonStyles = StyleSheet.create({
|
||||
btnIconHeader: { padding: 3 },
|
||||
btnFiturMenu: { padding: 13, borderRadius: 15, borderWidth: 1 },
|
||||
btnFiturMenu: { padding: 13, borderRadius: 20, borderWidth: 1 },
|
||||
btnRound: {
|
||||
backgroundColor: '#1F3C88',
|
||||
borderWidth: 0,
|
||||
@@ -37,8 +37,8 @@ const ButtonStyles = StyleSheet.create({
|
||||
padding: 5,
|
||||
borderWidth: 1,
|
||||
},
|
||||
labelStatus: { paddingHorizontal: 15, paddingVertical: 4, borderRadius: 10 },
|
||||
labelStatusSmall: { paddingHorizontal: 10, paddingVertical: 3, borderRadius: 10 },
|
||||
labelStatus: { paddingHorizontal: 15, paddingVertical: 4, borderRadius: 20 },
|
||||
labelStatusSmall: { paddingHorizontal: 10, paddingVertical: 3, borderRadius: 20 },
|
||||
});
|
||||
|
||||
export default ButtonStyles;
|
||||
|
||||
@@ -4,7 +4,7 @@ const CardStyles = StyleSheet.create({
|
||||
wrapPaper: {
|
||||
padding: 10,
|
||||
backgroundColor: 'white',
|
||||
borderRadius: 5,
|
||||
borderRadius: 8,
|
||||
shadowColor: '#171717',
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: 0.1,
|
||||
@@ -31,7 +31,7 @@ const CardStyles = StyleSheet.create({
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 5,
|
||||
elevation: 2,
|
||||
borderRadius: 5,
|
||||
borderRadius: 8,
|
||||
marginBottom: 15,
|
||||
},
|
||||
wrapGridCaraousel: {
|
||||
@@ -42,7 +42,7 @@ const CardStyles = StyleSheet.create({
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 5,
|
||||
elevation: 2,
|
||||
borderRadius: 5,
|
||||
borderRadius: 8,
|
||||
marginLeft: 5,
|
||||
display: 'flex',
|
||||
},
|
||||
@@ -57,27 +57,27 @@ const CardStyles = StyleSheet.create({
|
||||
paddingVertical: 25,
|
||||
paddingHorizontal: 20,
|
||||
alignItems: 'center',
|
||||
borderTopStartRadius: 5,
|
||||
borderTopEndRadius: 5,
|
||||
borderTopStartRadius: 8,
|
||||
borderTopEndRadius: 8,
|
||||
},
|
||||
contentPaperGrid: {
|
||||
height: 125,
|
||||
borderBottomEndRadius: 5,
|
||||
borderBottomStartRadius: 5,
|
||||
borderBottomEndRadius: 8,
|
||||
borderBottomStartRadius: 8,
|
||||
paddingHorizontal: 20,
|
||||
justifyContent: 'space-evenly',
|
||||
},
|
||||
contentPaperGrid2: {
|
||||
height: 100,
|
||||
borderBottomEndRadius: 5,
|
||||
borderBottomStartRadius: 5,
|
||||
borderBottomEndRadius: 8,
|
||||
borderBottomStartRadius: 8,
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 15,
|
||||
justifyContent: 'flex-start',
|
||||
},
|
||||
wrapGridItem: {
|
||||
borderWidth: 1,
|
||||
borderRadius: 5,
|
||||
borderRadius: 8,
|
||||
padding: 10,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
@@ -88,13 +88,13 @@ const CardStyles = StyleSheet.create({
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'transparent',
|
||||
borderRadius: 10,
|
||||
borderRadius: 8,
|
||||
borderWidth: 1,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
gap: 12,
|
||||
},
|
||||
sectionCard: { borderRadius: 12, padding: 16, borderWidth: 1 },
|
||||
sectionCard: { borderRadius: 8, padding: 16, borderWidth: 1 },
|
||||
sectionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 12 },
|
||||
sectionHeaderRow: {
|
||||
flexDirection: 'row',
|
||||
@@ -110,12 +110,12 @@ const CardStyles = StyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
},
|
||||
sectionActionRow: { flexDirection: 'row', alignItems: 'center', gap: 10 },
|
||||
sectionBadge: { borderRadius: 10, paddingHorizontal: 8, paddingVertical: 2 },
|
||||
sectionBadge: { borderRadius: 20, paddingHorizontal: 8, paddingVertical: 2 },
|
||||
wrapBar: { height: 20, backgroundColor: '#ccc', borderRadius: 10, margin: 0, width: '100%' },
|
||||
contentBar: { height: 20, backgroundColor: '#3B82F6', borderRadius: 10 },
|
||||
toastContainer: {
|
||||
backgroundColor: 'white',
|
||||
borderRadius: 10,
|
||||
borderRadius: 8,
|
||||
padding: 10,
|
||||
width: '90%',
|
||||
borderWidth: 1,
|
||||
@@ -135,7 +135,7 @@ const CardStyles = StyleSheet.create({
|
||||
loadingBox: {
|
||||
paddingVertical: 15,
|
||||
paddingHorizontal: 40,
|
||||
borderRadius: 5,
|
||||
borderRadius: 8,
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
},
|
||||
@@ -143,16 +143,16 @@ const CardStyles = StyleSheet.create({
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
marginHorizontal: 15,
|
||||
borderRadius: 15,
|
||||
borderRadius: 8,
|
||||
backgroundColor: '#19345E',
|
||||
display: 'flex',
|
||||
width: '92%',
|
||||
resizeMode: 'stretch',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
wrapItemDiscussion: { padding: 15, borderRadius: 5, borderBottomWidth: 1 },
|
||||
wrapItemDiscussion: { padding: 15, borderRadius: 8, borderBottomWidth: 1 },
|
||||
wrapItemBorderBottom: { padding: 10, borderBottomWidth: 1 },
|
||||
wrapItemBorderAll: { padding: 10, borderWidth: 1, borderRadius: 5, marginBottom: 5 },
|
||||
wrapItemBorderAll: { padding: 10, borderWidth: 1, borderRadius: 8, marginBottom: 5 },
|
||||
wrapItemBorderNone: { padding: 10, marginBottom: 5 },
|
||||
});
|
||||
|
||||
|
||||
@@ -5,14 +5,14 @@ const ComponentStyles = StyleSheet.create({
|
||||
userProfileExtraSmall: { width: 35, height: 35, borderRadius: 100 },
|
||||
userProfileSmall: { width: 48, height: 48, borderRadius: 100 },
|
||||
userProfileBig: { width: 100, height: 100, borderRadius: 100 },
|
||||
imgListBanner: { width: 100, height: 50, borderRadius: 5 },
|
||||
imgListBanner: { width: 100, height: 50, borderRadius: 8 },
|
||||
iconContent: { padding: 10, borderRadius: 100, backgroundColor: '#E5E7EB' },
|
||||
|
||||
// chip
|
||||
chip: {
|
||||
paddingVertical: 5,
|
||||
paddingHorizontal: 15,
|
||||
borderRadius: 5,
|
||||
borderRadius: 20,
|
||||
borderWidth: 1,
|
||||
borderColor: "transparent",
|
||||
marginRight: 10,
|
||||
@@ -26,14 +26,14 @@ const ComponentStyles = StyleSheet.create({
|
||||
top: -6,
|
||||
left: -6,
|
||||
backgroundColor: "#384288",
|
||||
borderRadius: 10,
|
||||
borderRadius: 8,
|
||||
padding: 2,
|
||||
},
|
||||
|
||||
// badge & progress
|
||||
badgeCol: { alignItems: 'center', gap: 6 },
|
||||
progressBadge: { borderRadius: 10, paddingHorizontal: 12, paddingVertical: 5, borderWidth: 1, alignItems: 'center' },
|
||||
taskCountBadge: { borderRadius: 6, paddingHorizontal: 7, paddingVertical: 2 },
|
||||
progressBadge: { borderRadius: 8, paddingHorizontal: 12, paddingVertical: 5, borderWidth: 1, alignItems: 'center' },
|
||||
taskCountBadge: { borderRadius: 8, paddingHorizontal: 7, paddingVertical: 2 },
|
||||
positionBadge: { borderRadius: 20, paddingHorizontal: 8, paddingVertical: 3 },
|
||||
textProgressPercent: { fontSize: 22, fontWeight: 'bold', lineHeight: 28 },
|
||||
progressTrack: { height: 8, borderRadius: 4, overflow: 'hidden' },
|
||||
@@ -41,11 +41,11 @@ const ComponentStyles = StyleSheet.create({
|
||||
reportContent: { borderLeftWidth: 3, paddingLeft: 12 },
|
||||
expandBtn: { flexDirection: 'row', alignItems: 'center', alignSelf: 'flex-start', marginTop: 8, gap: 4 },
|
||||
fileGrid: { flexDirection: 'row', flexWrap: 'wrap', gap: 8 },
|
||||
fileCard: { width: '48%', borderRadius: 10, borderWidth: 1, padding: 12, flexDirection: 'row', alignItems: 'center', gap: 10 },
|
||||
fileCard: { width: '48%', borderRadius: 8, borderWidth: 1, padding: 12, flexDirection: 'row', alignItems: 'center', gap: 10 },
|
||||
|
||||
// calendar
|
||||
signDate: { width: 20, height: 2, borderRadius: 3, marginTop: 3 },
|
||||
selectedDate: { backgroundColor: '#238be6', borderRadius: 5 },
|
||||
signDate: { width: 20, height: 2, borderRadius: 4, marginTop: 3 },
|
||||
selectedDate: { backgroundColor: '#238be6', borderRadius: 4 },
|
||||
selectRangeDate: { backgroundColor: '#228be61f' },
|
||||
calendarDotRow: { flexDirection: 'row', gap: 2, height: 6, marginTop: 1 },
|
||||
calendarDot: { width: 5, height: 5, borderRadius: 3 },
|
||||
@@ -55,7 +55,7 @@ const ComponentStyles = StyleSheet.create({
|
||||
villageEventBadge: { paddingHorizontal: 6, paddingVertical: 2, borderRadius: 4, marginRight: 6 },
|
||||
|
||||
// event item
|
||||
itemEvent: { padding: 10, borderRadius: 10, flexDirection: 'row', alignContent: 'stretch', marginBottom: 10 },
|
||||
itemEvent: { padding: 10, borderRadius: 8, flexDirection: 'row', alignContent: 'stretch', marginBottom: 10 },
|
||||
dividerEvent: { width: 7, borderRadius: 5, marginRight: 10 },
|
||||
|
||||
// member
|
||||
@@ -75,16 +75,16 @@ const ComponentStyles = StyleSheet.create({
|
||||
memberInfoContent: { flex: 1, marginLeft: 10 },
|
||||
|
||||
// discussion
|
||||
discussionCard: { borderRadius: 10, borderWidth: 1, padding: 14 },
|
||||
discussionIconCircle: { width: 40, height: 40, borderRadius: 20, alignItems: 'center', justifyContent: 'center', flexShrink: 0 },
|
||||
discussionIconCircleLg: { width: 44, height: 44, borderRadius: 22, alignItems: 'center', justifyContent: 'center' },
|
||||
discussionCard: { borderRadius: 8, borderWidth: 1, padding: 14 },
|
||||
discussionIconCircle: { width: 40, height: 40, borderRadius: 100, alignItems: 'center', justifyContent: 'center', flexShrink: 0 },
|
||||
discussionIconCircleLg: { width: 44, height: 44, borderRadius: 100, alignItems: 'center', justifyContent: 'center' },
|
||||
discussionStatusPill: { alignSelf: 'flex-start', marginTop: 3, paddingHorizontal: 8, paddingVertical: 2, borderRadius: 20, borderWidth: 1 },
|
||||
discussionStatusText: { fontSize: 11, fontWeight: '600' },
|
||||
discussionCardIndent: { marginLeft: 50 },
|
||||
discussionSeparator: { height: 8 },
|
||||
discussionCommentText: { fontSize: 12, marginLeft: 5 },
|
||||
discussionDateText: { fontSize: 11 },
|
||||
discussionCommentCard: { borderRadius: 10, borderWidth: 1, padding: 12, marginBottom: 8, flexDirection: 'row' },
|
||||
discussionCommentCard: { borderRadius: 8, borderWidth: 1, padding: 12, marginBottom: 8, flexDirection: 'row' },
|
||||
discussionEditedText: { fontSize: 10, fontStyle: 'italic' },
|
||||
discussionHeaderPadding: { paddingTop: 12 },
|
||||
discussionListPadding: { paddingTop: 8 },
|
||||
@@ -98,7 +98,7 @@ const ComponentStyles = StyleSheet.create({
|
||||
position: 'absolute',
|
||||
left: 24,
|
||||
right: 24,
|
||||
borderRadius: 16,
|
||||
borderRadius: 8,
|
||||
padding: 20,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
|
||||
@@ -2,7 +2,7 @@ import { StyleSheet } from "react-native";
|
||||
|
||||
const InputStyles = StyleSheet.create({
|
||||
inputRoundForm: {
|
||||
borderRadius: 5,
|
||||
borderRadius: 12,
|
||||
borderColor: '#d6d8f6',
|
||||
borderWidth: 1,
|
||||
paddingVertical: 10,
|
||||
@@ -25,7 +25,7 @@ const InputStyles = StyleSheet.create({
|
||||
lineHeight: 45,
|
||||
fontSize: 24,
|
||||
borderWidth: 1,
|
||||
borderRadius: 15,
|
||||
borderRadius: 12,
|
||||
borderColor: 'gray',
|
||||
textAlign: 'center',
|
||||
},
|
||||
|
||||
@@ -6,31 +6,31 @@ const ModalStyles = StyleSheet.create({
|
||||
width: '100%',
|
||||
paddingBottom: 20,
|
||||
backgroundColor: 'white',
|
||||
borderTopRightRadius: 18,
|
||||
borderTopLeftRadius: 18,
|
||||
borderTopRightRadius: 20,
|
||||
borderTopLeftRadius: 20,
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
},
|
||||
modalContentNew: {
|
||||
width: '100%',
|
||||
backgroundColor: 'white',
|
||||
borderTopRightRadius: 18,
|
||||
borderTopLeftRadius: 18,
|
||||
borderTopRightRadius: 20,
|
||||
borderTopLeftRadius: 20,
|
||||
paddingTop: 5,
|
||||
paddingBottom: 5,
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
modalFloatContent: {
|
||||
backgroundColor: 'white',
|
||||
borderRadius: 18,
|
||||
borderRadius: 20,
|
||||
paddingTop: 5,
|
||||
paddingBottom: 10,
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
titleContainer: {
|
||||
backgroundColor: 'white',
|
||||
borderTopRightRadius: 10,
|
||||
borderTopLeftRadius: 10,
|
||||
borderTopRightRadius: 20,
|
||||
borderTopLeftRadius: 20,
|
||||
paddingHorizontal: 20,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
@@ -65,7 +65,7 @@ const ModalStyles = StyleSheet.create({
|
||||
},
|
||||
modalConfirmContainer: {
|
||||
width: '80%',
|
||||
borderRadius: 14,
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden',
|
||||
elevation: 2,
|
||||
shadowColor: '#000',
|
||||
@@ -92,7 +92,7 @@ const ModalStyles = StyleSheet.create({
|
||||
position: 'absolute',
|
||||
width: 300,
|
||||
height: 300,
|
||||
borderRadius: 150,
|
||||
borderRadius: 100,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
||||
top: -50,
|
||||
right: -50,
|
||||
|
||||
@@ -8,7 +8,7 @@ const NotificationStyles = StyleSheet.create({
|
||||
notifItemRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderRadius: 10,
|
||||
borderRadius: 8,
|
||||
borderWidth: 1,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
@@ -17,7 +17,7 @@ const NotificationStyles = StyleSheet.create({
|
||||
notifIconContainer: {
|
||||
width: 42,
|
||||
height: 42,
|
||||
borderRadius: 21,
|
||||
borderRadius: 100,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
|
||||
@@ -394,7 +394,7 @@
|
||||
);
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = mobiledarmasaba.app;
|
||||
PRODUCT_NAME = Desa;
|
||||
PRODUCT_NAME = "Desa";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Desa/Desa-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
@@ -429,7 +429,7 @@
|
||||
);
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = mobiledarmasaba.app;
|
||||
PRODUCT_NAME = Desa;
|
||||
PRODUCT_NAME = "Desa";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Desa/Desa-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.1.0</string>
|
||||
<string>2.2.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
@@ -39,7 +39,7 @@
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>9</string>
|
||||
<string>10</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
|
||||
@@ -39,7 +39,14 @@ export const apiSendDiscussionGeneralCommentar = async ({ id, data }: { id: stri
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const apiUpdateDiscussionGeneralCommentar = async ({ id, data }: { id: string, data: { desc: string, user: string } }) => {
|
||||
export const apiSendDiscussionGeneralCommentarWithFile = async (id: string, data: FormData) => {
|
||||
const response = await api.post(`/mobile/discussion-general/${id}/comment`, data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
})
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const apiUpdateDiscussionGeneralCommentar = async ({ id, data }: { id: string, data: { desc: string, user: string, filesToRemove?: string[] } }) => {
|
||||
const response = await api.put(`/mobile/discussion-general/${id}/comment`, data)
|
||||
return response.data;
|
||||
};
|
||||
|
||||
37
plugins/withRemoveMediaPermissions.js
Normal file
37
plugins/withRemoveMediaPermissions.js
Normal file
@@ -0,0 +1,37 @@
|
||||
const { withAndroidManifest } = require('@expo/config-plugins');
|
||||
|
||||
const BLOCKED_PERMISSIONS = [
|
||||
'android.permission.READ_MEDIA_IMAGES',
|
||||
'android.permission.READ_MEDIA_VIDEO',
|
||||
];
|
||||
|
||||
const withRemoveMediaPermissions = (config) =>
|
||||
withAndroidManifest(config, (config) => {
|
||||
const manifest = config.modResults.manifest;
|
||||
|
||||
// Pastikan xmlns:tools ada di manifest root
|
||||
if (!manifest.$['xmlns:tools']) {
|
||||
manifest.$['xmlns:tools'] = 'http://schemas.android.com/tools';
|
||||
}
|
||||
|
||||
// Hapus entry yang ada (apapun atributnya)
|
||||
const existing = manifest['uses-permission'] ?? [];
|
||||
manifest['uses-permission'] = existing.filter(
|
||||
(perm) => !BLOCKED_PERMISSIONS.includes(perm.$?.['android:name'])
|
||||
);
|
||||
|
||||
// Tambahkan entry dengan tools:node="remove" agar Gradle merger
|
||||
// membuang permission ini dari SEMUA sumber (termasuk library manifests)
|
||||
for (const permission of BLOCKED_PERMISSIONS) {
|
||||
manifest['uses-permission'].push({
|
||||
$: {
|
||||
'android:name': permission,
|
||||
'tools:node': 'remove',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return config;
|
||||
});
|
||||
|
||||
module.exports = withRemoveMediaPermissions;
|
||||
Reference in New Issue
Block a user