Files
mobile-darmasaba/components/discussion_general/modalSelectMemberByDivision.tsx

288 lines
13 KiB
TypeScript

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