diff --git a/app/(application)/discussion/create.tsx b/app/(application)/discussion/create.tsx index 6a0fa7a..2992bf9 100644 --- a/app/(application)/discussion/create.tsx +++ b/app/(application)/discussion/create.tsx @@ -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() { 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} /> + diff --git a/components/discussion_general/modalSelectMemberByDivision.tsx b/components/discussion_general/modalSelectMemberByDivision.tsx new file mode 100644 index 0000000..1a69556 --- /dev/null +++ b/components/discussion_general/modalSelectMemberByDivision.tsx @@ -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([]) + const [selectMember, setSelectMember] = useState([]) + const [search, setSearch] = useState('') + const [loadingDivisions, setLoadingDivisions] = useState(false) + const [loadingIds, setLoadingIds] = useState([]) + const [searchResults, setSearchResults] = useState([]) + 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 { + 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 ( + + + {selectMember.length > 0 + ? ( + + + {selectMember.map((item, index) => ( + handleToggleMember(item)} + /> + ))} + + + ) + : ( + + Tidak ada member yang dipilih + + ) + } + + + {search ? ( + loadingSearch ? ( + + ) : searchResults.length > 0 ? ( + searchResults.map((item, idx) => ( + handleToggleMember({ idUser: item.id, name: item.name, img: item.img })} + > + + + {item.name} + + {selectMember.some(s => s.idUser === item.id) && ( + + )} + + )) + ) : ( + + Tidak ada hasil + + ) + ) : loadingDivisions ? ( + + ) : 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 ( + + handleTapDivision(division)} + > + + {division.name} + {division.membersLoaded && ( + + {selectedCount > 0 + ? `${selectedCount} dari ${division.members.length} dipilih` + : `${division.members.length} anggota`} + + )} + + {isLoadingThis ? ( + + ) : allSelected ? ( + + ) : someSelected ? ( + + ) : null} + handleToggleExpand(division.id)} + style={{ paddingLeft: 10 }} + hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} + > + + + + + {division.expanded && division.members.map((member, idx) => ( + handleToggleMember(member)} + > + + + {member.name} + + {selectMember.some(s => s.idUser === member.idUser) && ( + + )} + + ))} + + ) + }) + ) : ( + + Tidak ada divisi + + )} + + + + + + ) +}