index.tsx 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. import React, { useCallback, useEffect, useState } from 'react';
  2. import { useFocusEffect } from '@react-navigation/native';
  3. import { View, Text, FlatList } from 'react-native';
  4. import { Button, Header, Input, Loading, Modal, PageWrapper } from 'src/components';
  5. import { TabView, TabBar } from 'react-native-tab-view';
  6. import SearchIcon from '../../../../../assets/icons/search.svg';
  7. import { fetchItemsForSeries, usePostSetToggleItem } from '@api/series';
  8. import { Colors } from 'src/theme';
  9. import { ButtonVariants } from 'src/types/components';
  10. import { AccordionListItem } from '../Components/AccordionListItem';
  11. import { styles } from './styles';
  12. interface SeriesItem {
  13. series_id: number;
  14. item_id: number;
  15. icon: string | null;
  16. name: string;
  17. readonly: boolean;
  18. info: string;
  19. new: boolean;
  20. checked: boolean;
  21. checked_double: boolean;
  22. double: boolean;
  23. }
  24. interface SeriesGroup {
  25. name: string;
  26. icon: string;
  27. series_id: number;
  28. series_name: string;
  29. items: SeriesItem[];
  30. }
  31. interface FilteredData {
  32. [key: string]: SeriesGroup[];
  33. }
  34. interface Route {
  35. key: string;
  36. title: string;
  37. }
  38. export const SeriesItemScreen = ({ route }: { route: any }) => {
  39. const { id, name, token } = route.params;
  40. const { mutate: updateSeriesItem } = usePostSetToggleItem();
  41. const [search, setSearch] = useState<string>('');
  42. const [filteredData, setFilteredData] = useState<FilteredData>({});
  43. const [seriesData, setSeriesData] = useState<SeriesGroup[]>([]);
  44. const [index, setIndex] = useState<number>(0);
  45. const [isLoading, setIsLoading] = useState<boolean>(true);
  46. const [isInfoModalVisible, setIsInfoModalVisible] = useState<boolean>(false);
  47. const [infoItem, setInfoItem] = useState<SeriesItem | null>(null);
  48. const [activeFilteredData, setActiveFilteredData] = useState<SeriesGroup[]>([]);
  49. useFocusEffect(
  50. useCallback(() => {
  51. const fetchGroups = async () => {
  52. const data = await fetchItemsForSeries(token, id);
  53. if (!data) {
  54. setIsLoading(false);
  55. return;
  56. }
  57. const allItems = data.groups;
  58. const newItems = data.groups
  59. .map((group) => ({
  60. ...group,
  61. items: group.items.filter((item) => item.new)
  62. }))
  63. .filter((group) => group.items.length > 0);
  64. setFilteredData({ all: allItems, new: newItems, unchecked: [], checked: [] });
  65. setSeriesData(data.groups);
  66. setIsLoading(false);
  67. };
  68. fetchGroups();
  69. }, [])
  70. );
  71. const [routes] = useState([
  72. { key: 'all', title: 'All items' },
  73. { key: 'new', title: 'New' },
  74. { key: 'unchecked', title: 'Unchecked' },
  75. { key: 'checked', title: 'Checked' }
  76. ]);
  77. const handleIndexChange = (index: number) => {
  78. let dataToFilter = [...seriesData];
  79. switch (index) {
  80. case 1:
  81. dataToFilter = filteredData[routes[index].key];
  82. break;
  83. case 2:
  84. dataToFilter = dataToFilter
  85. .map((group) => ({
  86. ...group,
  87. items: group.items.filter((item) => !item.checked)
  88. }))
  89. .filter((group) => group.items.length > 0);
  90. break;
  91. case 3:
  92. dataToFilter = dataToFilter
  93. .map((group) => ({
  94. ...group,
  95. items: group.items.filter((item) => item.checked)
  96. }))
  97. .filter((group) => group.items.length > 0);
  98. break;
  99. default:
  100. break;
  101. }
  102. setActiveFilteredData(dataToFilter);
  103. setFilteredData((prevState) => ({
  104. ...prevState,
  105. [routes[index].key]: dataToFilter
  106. }));
  107. };
  108. useEffect(() => {
  109. handleIndexChange(index);
  110. }, [seriesData]);
  111. useEffect(() => {
  112. setActiveFilteredData(filteredData[routes[index].key]);
  113. }, [filteredData]);
  114. const renderScene = ({ route }: { route: Route }) => {
  115. return isLoading ? (
  116. <Loading />
  117. ) : (
  118. <FlatList
  119. key={routes[index].key}
  120. keyExtractor={(item, index) => index.toString()}
  121. showsVerticalScrollIndicator={false}
  122. initialNumToRender={15}
  123. style={{ paddingTop: 10 }}
  124. data={activeFilteredData}
  125. renderItem={({ item }) => (
  126. <AccordionListItem
  127. item={item}
  128. onCheckboxChange={handleCheckboxChange}
  129. setIsInfoModalVisible={setIsInfoModalVisible}
  130. setInfoItem={setInfoItem}
  131. isSeries={id === -1}
  132. />
  133. )}
  134. />
  135. );
  136. };
  137. useEffect(() => {
  138. const searchText = search.toLowerCase();
  139. const searchData = filteredData[routes[index].key]
  140. ?.map((group) => {
  141. const groupNameMatch = group.name.toLowerCase().includes(searchText);
  142. if (groupNameMatch) {
  143. return group;
  144. } else {
  145. const filteredItems = group.items.filter((item) =>
  146. item.name.toLowerCase().includes(searchText)
  147. );
  148. return filteredItems.length > 0 ? { ...group, items: filteredItems } : null;
  149. }
  150. })
  151. .filter((group) => group !== null);
  152. setActiveFilteredData(searchData as SeriesGroup[]);
  153. }, [search]);
  154. const handleCheckboxChange = useCallback(
  155. async (item: SeriesItem, groupName: string, double?: boolean) => {
  156. setSeriesData((currentData) => {
  157. const groupIndex = currentData.findIndex((group) => group.name === groupName);
  158. if (groupIndex === -1) return currentData;
  159. const newData = [...currentData];
  160. const newGroup = { ...newData[groupIndex] };
  161. newGroup.items = newGroup.items.map((subItem) =>
  162. subItem.item_id === item.item_id
  163. ? {
  164. ...subItem,
  165. ...(double
  166. ? { checked_double: !subItem.checked_double }
  167. : { checked: !subItem.checked })
  168. }
  169. : subItem
  170. );
  171. newData[groupIndex] = newGroup;
  172. return newData;
  173. });
  174. const itemData = {
  175. token: token,
  176. series_id: item.series_id,
  177. item_id: item.item_id,
  178. checked: (!double ? Number(!item.checked) : Number(item.checked)) as 0 | 1,
  179. double: (double ? Number(!item.checked_double) : Number(item.checked_double)) as 0 | 1
  180. };
  181. try {
  182. updateSeriesItem(itemData);
  183. } catch (error) {
  184. console.error('Failed to update checkbox state', error);
  185. }
  186. },
  187. [token, updateSeriesItem]
  188. );
  189. return (
  190. <PageWrapper>
  191. <Header label={name} />
  192. <Modal
  193. visible={isInfoModalVisible}
  194. children={
  195. <View style={styles.modalView}>
  196. <Text style={styles.infoTitle}>{infoItem?.name}</Text>
  197. <Text style={styles.infoText}>{infoItem?.info}</Text>
  198. <Button
  199. variant={ButtonVariants.OPACITY}
  200. containerStyles={styles.btnContainer}
  201. textStyles={{
  202. color: Colors.DARK_BLUE
  203. }}
  204. onPress={() => setIsInfoModalVisible(false)}
  205. children={'Got it'}
  206. />
  207. </View>
  208. }
  209. onRequestClose={() => setIsInfoModalVisible(false)}
  210. headerTitle={'Info'}
  211. visibleInPercent={'auto'}
  212. />
  213. <Input
  214. inputMode={'search'}
  215. placeholder={'Search'}
  216. onChange={(text) => setSearch(text)}
  217. value={search}
  218. icon={<SearchIcon fill={'#C8C8C8'} width={14} height={14} />}
  219. />
  220. <TabView
  221. navigationState={{ index, routes }}
  222. renderScene={renderScene}
  223. onIndexChange={(i) => {
  224. handleIndexChange(i);
  225. setIndex(i);
  226. }}
  227. lazy={true}
  228. renderTabBar={(props) => (
  229. <TabBar
  230. {...props}
  231. indicatorStyle={{ backgroundColor: Colors.DARK_BLUE }}
  232. style={styles.tabBar}
  233. tabStyle={styles.tabStyle}
  234. pressColor={'transparent'}
  235. renderLabel={({ route, focused }) => (
  236. <Text
  237. style={[styles.tabLabel, { color: Colors.DARK_BLUE, opacity: focused ? 1 : 0.4 }]}
  238. >
  239. {route.title}
  240. </Text>
  241. )}
  242. />
  243. )}
  244. />
  245. </PageWrapper>
  246. );
  247. };