| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359 |
- import React, { useCallback, useEffect, useRef, useState } from 'react';
- import { useFocusEffect } from '@react-navigation/native';
- import { View, Text, FlatList, Alert } from 'react-native';
- import { Button, Header, Input, Loading, Modal, PageWrapper } from 'src/components';
- import SearchIcon from '../../../../../assets/icons/search.svg';
- import { fetchItemsForSeries, usePostSetToggleItem } from '@api/series';
- import { Colors } from 'src/theme';
- import { ButtonVariants } from 'src/types/components';
- import { AccordionListItem } from '../Components/AccordionListItem';
- import { styles } from './styles';
- import TabViewWrapper from 'src/components/TabViewWrapper';
- import { SafeAreaView } from 'react-native-safe-area-context';
- interface SeriesItem {
- series_id: number;
- item_id: number;
- icon: string | null;
- name: string;
- readonly: boolean;
- info: string;
- new: boolean;
- checked: boolean;
- checked_double: boolean;
- double: boolean;
- }
- interface SeriesGroup {
- name: string;
- icon: string;
- series_id: number;
- series_name: string;
- items: SeriesItem[];
- }
- interface FilteredData {
- [key: string]: SeriesGroup[];
- }
- interface Route {
- key: string;
- title: string;
- }
- export const SeriesItemScreen = ({ route }: { route: any }) => {
- const { id, name, token, scrollToItemId } = route.params;
- const { mutate: updateSeriesItem } = usePostSetToggleItem();
- const flatListRef = useRef<FlatList>(null);
- const scrolledRef = useRef(false);
- const [search, setSearch] = useState<string>('');
- const [filteredData, setFilteredData] = useState<FilteredData>({});
- const [seriesData, setSeriesData] = useState<SeriesGroup[]>([]);
- const [index, setIndex] = useState<number>(0);
- const [isLoading, setIsLoading] = useState<boolean>(true);
- const [isInfoModalVisible, setIsInfoModalVisible] = useState<boolean>(false);
- const [infoItem, setInfoItem] = useState<SeriesItem | null>(null);
- const [activeFilteredData, setActiveFilteredData] = useState<SeriesGroup[]>([]);
- useFocusEffect(
- useCallback(() => {
- const fetchGroups = async () => {
- const data = await fetchItemsForSeries(String(token), id);
- if (!data) {
- setIsLoading(false);
- return;
- }
- const allItems = data.groups;
- const newItems = data.groups
- .map((group) => ({
- ...group,
- items: group.items.filter((item) => item.new)
- }))
- .filter((group) => group.items.length > 0);
- setFilteredData({ all: allItems, new: newItems, unchecked: [], checked: [] });
- setSeriesData(data.groups);
- setIsLoading(false);
- };
- fetchGroups();
- }, [])
- );
- const [routes] = useState([
- { key: 'all', title: 'All items' },
- { key: 'checked', title: 'Ticked' },
- { key: 'unchecked', title: 'Unticked' },
- { key: 'new', title: 'New' }
- ]);
- const handleIndexChange = (index: number) => {
- let dataToFilter = [...seriesData];
- switch (index) {
- case 3:
- dataToFilter = dataToFilter
- .map((group) => ({
- ...group,
- items: group.items.filter((item) => item.new)
- }))
- .filter((group) => group.items.length > 0);
- break;
- case 2:
- dataToFilter = dataToFilter
- .map((group) => ({
- ...group,
- items: group.items.filter(
- (item) => !item.checked || (item.double && !item.checked_double)
- )
- }))
- .filter((group) => group.items.length > 0);
- break;
- case 1:
- dataToFilter = dataToFilter
- .map((group) => ({
- ...group,
- items: group.items.filter(
- (item) => item.checked || (item.double && item.checked_double)
- )
- }))
- .filter((group) => group.items.length > 0);
- break;
- default:
- break;
- }
- setActiveFilteredData(dataToFilter);
- setFilteredData((prevState) => ({
- ...prevState,
- [routes[index].key]: dataToFilter
- }));
- };
- useEffect(() => {
- handleIndexChange(index);
- }, [seriesData, index]);
- useEffect(() => {
- setActiveFilteredData(filteredData[routes[index].key]);
- }, [filteredData]);
- const [subItemY, setSubItemY] = useState<number | null>(null);
- const [groupY, setGroupY] = useState<number | null>(null);
- useEffect(() => {
- if (!scrollToItemId || scrolledRef.current || !activeFilteredData?.length) return;
-
- const targetId = String(scrollToItemId);
- const groupIndex = activeFilteredData.findIndex((group) =>
- group.items.some((item) => String(item.item_id) === targetId)
- );
-
- if (groupIndex !== -1) {
- if (subItemY !== null && groupY !== null) {
- scrolledRef.current = true;
-
- const scroll = () => {
- flatListRef.current?.scrollToIndex({
- index: groupIndex,
- animated: true,
- viewPosition: 0,
- viewOffset: -subItemY - 60 + 50 // -subItemY to push group up, -60 for header, +50 buffer
- });
- };
-
- const timer1 = setTimeout(scroll, 300);
- const timer2 = setTimeout(scroll, 800);
-
- return () => {
- clearTimeout(timer1);
- clearTimeout(timer2);
- };
- } else {
- const roughScroll = () => {
- flatListRef.current?.scrollToIndex({
- index: groupIndex,
- animated: false,
- viewPosition: 0
- });
- };
- const timer = setTimeout(roughScroll, 600);
- return () => clearTimeout(timer);
- }
- } else {
- scrolledRef.current = true;
- }
- }, [scrollToItemId, activeFilteredData, subItemY, groupY]);
- const renderScene = ({ route: tabRoute }: { route: Route }) => {
- return isLoading ? (
- <Loading />
- ) : (
- <FlatList
- ref={flatListRef}
- key={routes[index].key}
- keyExtractor={(item, index) => index.toString()}
- showsVerticalScrollIndicator={false}
- initialNumToRender={15}
- windowSize={11}
- style={{ paddingTop: 10 }}
- contentContainerStyle={{ paddingBottom: 16 }}
- data={activeFilteredData}
- onScrollToIndexFailed={(info) => {
- const offset = info.averageItemLength * info.index;
- flatListRef.current?.scrollToOffset({ offset, animated: false });
- setTimeout(() => {
- flatListRef.current?.scrollToIndex({
- index: info.index,
- animated: false,
- viewPosition: 0
- });
- }, 150);
- }}
- renderItem={({ item, index }) => {
- const targetId = String(scrollToItemId);
- const isTargetGroup = item.items.some((subItem) => String(subItem.item_id) === targetId);
-
- return (
- <View
- onLayout={(e) => {
- if (isTargetGroup) {
- const y = e.nativeEvent.layout.y;
- setGroupY((prev) => (prev === null ? y : prev));
- }
- }}
- >
- <AccordionListItem
- item={item}
- onCheckboxChange={handleCheckboxChange}
- setIsInfoModalVisible={setIsInfoModalVisible}
- setInfoItem={setInfoItem}
- isSeries={id === -1}
- token={token}
- highlightItemId={scrollToItemId}
- onHighlightLayout={(y) => {
- setSubItemY((prev) => (prev === null ? y : prev));
- }}
- />
- </View>
- );
- }}
- />
- );
- };
- useEffect(() => {
- const searchText = search.toLowerCase();
- const searchData = filteredData[routes[index].key]
- ?.map((group) => {
- const groupNameMatch = group.name.toLowerCase().includes(searchText);
- if (groupNameMatch) {
- return group;
- } else {
- const filteredItems = group.items.filter((item) =>
- item.name.toLowerCase().includes(searchText)
- );
- return filteredItems.length > 0 ? { ...group, items: filteredItems } : null;
- }
- })
- .filter((group) => group !== null);
- setActiveFilteredData(searchData as SeriesGroup[]);
- }, [search]);
- const handleCheckboxChange = useCallback(
- async (item: SeriesItem, groupName: string, double?: boolean) => {
- setSeriesData((currentData) => {
- const newData = currentData.map((group) => {
- const hasItem = group.items.some((subItem) => subItem.item_id === item.item_id);
- if (!hasItem) return group;
- return {
- ...group,
- items: group.items.map((subItem) =>
- subItem.item_id === item.item_id
- ? {
- ...subItem,
- ...(double
- ? { checked_double: !subItem.checked_double }
- : { checked: !subItem.checked })
- }
- : subItem
- )
- };
- });
- return newData;
- });
- const itemData = {
- token: token,
- series_id: item.series_id,
- item_id: item.item_id,
- checked: double ? (item.checked_double ? 0 : 1) : (item.checked ? 0 : 1) as 0 | 1,
- double: (double ? 1 : 0) as 0 | 1
- };
- try {
- updateSeriesItem(itemData);
- } catch (error) {
- console.error('Failed to update checkbox state', error);
- }
- },
- [token, updateSeriesItem]
- );
- return (
- <SafeAreaView style={{ height: '100%' }} edges={['top']}>
- <View style={{ marginLeft: '5%', marginRight: '5%' }}>
- <Header label={name} />
- <Input
- inputMode={'search'}
- placeholder={'Search'}
- onChange={(text) => setSearch(text)}
- value={search}
- icon={<SearchIcon fill={'#C8C8C8'} width={14} height={14} />}
- />
- </View>
- <Modal
- visible={isInfoModalVisible}
- children={
- <View style={styles.modalView}>
- <Text style={styles.infoTitle}>{infoItem?.name}</Text>
- <Text style={styles.infoText}>{infoItem?.info}</Text>
- <Button
- variant={ButtonVariants.OPACITY}
- containerStyles={styles.btnContainer}
- textStyles={{
- color: Colors.DARK_BLUE
- }}
- onPress={() => setIsInfoModalVisible(false)}
- children={'Got it'}
- />
- </View>
- }
- onRequestClose={() => setIsInfoModalVisible(false)}
- headerTitle={'Info'}
- visibleInPercent={'auto'}
- />
- {token ? (
- <TabViewWrapper
- routes={routes}
- renderScene={renderScene as never}
- setIndex={setIndex}
- lazy={false}
- selectedIndex={index}
- />
- ) : (
- <View style={{ paddingTop: 8, flex: 1, marginHorizontal: '5%' }}>
- {renderScene({ route: routes[index] })}
- </View>
- )}
- </SafeAreaView>
- );
- };
|