From 9cd78dae3acfced1d050daaff303ff76026d8b17 Mon Sep 17 00:00:00 2001 From: amaliadwiy Date: Tue, 9 Jun 2026 15:25:22 +0800 Subject: [PATCH] feat: ubah halaman tambah anggota diskusi umum menggunakan pilih anggota berdasarkan divisi --- .../discussion/add-member/[id].tsx | 360 ++++++++++++------ 1 file changed, 253 insertions(+), 107 deletions(-) diff --git a/app/(application)/discussion/add-member/[id].tsx b/app/(application)/discussion/add-member/[id].tsx index f60275f..4aa3415 100644 --- a/app/(application)/discussion/add-member/[id].tsx +++ b/app/(application)/discussion/add-member/[id].tsx @@ -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([]) - const [data, setData] = useState([]) + const [dataOld, setDataOld] = useState([]) const [idGroup, setIdGroup] = useState('') - const [selectMember, setSelectMember] = useState([]) + const [selectMember, setSelectMember] = useState([]) const [search, setSearch] = useState('') const [loading, setLoading] = useState(false) + const [divisions, setDivisions] = useState([]) + const [loadingDivisions, setLoadingDivisions] = useState(false) + const [loadingIds, setLoadingIds] = useState([]) + const [searchResults, setSearchResults] = useState([]) + 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 { + 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 ( <> { router.back() }} />, - headerTitle: 'Tambah Anggota Diskusi', - headerTitleAlign: 'center', - // headerRight: () => ( - // { - // handleAddMember() - // }} - // /> - // ) header: () => ( { - handleAddMember() - }} + disable={selectMember.length === 0 || loading} + onPress={handleAddMember} /> } /> @@ -131,65 +204,138 @@ export default function AddMemberDiscussionDetail() { /> - - { - selectMember.length > 0 - ? - - - { - selectMember.map((item: any, index: any) => ( - onChoose(item.idUser, item.name, item.img)} - /> - )) - } - - - - : - Tidak ada member yang dipilih - } - - - { - data.length > 0 ? - data.map((item: any, index: any) => { - const found = dataOld.some((i: any) => i.idUser == item.id) - return ( - { - !found && onChoose(item.id, item.name, item.img) - }} - > - - - - {item.name} - { - found && sudah menjadi anggota - } + {selectMember.length > 0 ? ( + + + {selectMember.map((item, index) => ( + handleToggleMember(item)} + /> + ))} + + + ) : ( + + Tidak ada member yang dipilih + + )} + + + {search ? ( + loadingSearch ? ( + + ) : searchResults.length > 0 ? ( + searchResults.map((item, idx) => { + const isOld = dataOld.some((o: any) => o.idUser === item.idUser) + return ( + !isOld && handleToggleMember(item)} + > + + + + {item.name} + {isOld && ( + sudah menjadi anggota + )} + - - { - selectMember.some((i: any) => i.idUser == item.id) && - } - - ) - } + {selectMember.some(s => s.idUser === item.idUser) && ( + + )} + + ) + }) + ) : ( + + Tidak ada hasil + ) - : - Tidak ada data - } + ) : loadingDivisions ? ( + + ) : 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 ( + + handleTapDivision(division)} + > + + {division.name} + {division.membersLoaded && ( + + {selectedCount > 0 + ? `${selectedCount} dari ${eligible.length} dipilih` + : `${eligible.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) => { + const isOld = dataOld.some((o: any) => o.idUser === member.idUser) + return ( + !isOld && handleToggleMember(member)} + > + + + + {member.name} + {isOld && ( + sudah menjadi anggota + )} + + + {!isOld && selectMember.some(s => s.idUser === member.idUser) && ( + + )} + + ) + })} + + ) + }) + ) : ( + + Tidak ada divisi + + )} + ) -} \ No newline at end of file +}