index.tsx 31 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015
  1. import React, { useState, useEffect, useCallback, useRef } from 'react';
  2. import {
  3. View,
  4. Text,
  5. StyleSheet,
  6. TouchableOpacity,
  7. ScrollView,
  8. Alert,
  9. Animated,
  10. Easing
  11. } from 'react-native';
  12. import { SafeAreaView } from 'react-native-safe-area-context';
  13. import DateTimePicker from '@react-native-community/datetimepicker';
  14. import { Picker } from '@react-native-picker/picker';
  15. import { MaterialCommunityIcons } from '@expo/vector-icons';
  16. import { Picker as WheelPicker } from 'react-native-wheel-pick';
  17. import moment from 'moment';
  18. import { Button, Header, PageWrapper, WarningModal } from 'src/components';
  19. import { Colors } from 'src/theme';
  20. import { Dropdown } from 'react-native-searchable-dropdown-kj';
  21. import { qualityOptions } from '../utils/constants';
  22. import { getFontSize } from 'src/utils';
  23. import TrashSVG from 'assets/icons/travels-screens/trash-solid.svg';
  24. import { storage, StoreType } from 'src/storage';
  25. import {
  26. useGetVisitsQuery,
  27. usePostAddVisitMutation,
  28. usePostDeleteVisitMutation,
  29. usePostGetSingleRegionMutation,
  30. usePostUpdateVisitMutation
  31. } from '@api/myRegions';
  32. import ActionSheet from 'react-native-actions-sheet';
  33. import { ButtonVariants } from 'src/types/components';
  34. import EditSvg from 'assets/icons/travels-screens/pen-to-square.svg';
  35. import TripIcon from 'assets/icons/travels-section/trip.svg';
  36. import { useRegion } from 'src/contexts/RegionContext';
  37. import { NmRegion } from '../utils/types';
  38. type DateMode = 'year' | 'month' | 'full';
  39. type QualityType = {
  40. id: number;
  41. name: string;
  42. };
  43. interface DateValue {
  44. year: number | null;
  45. month: number | null;
  46. day: number | null;
  47. }
  48. interface Visit {
  49. id: number;
  50. trip_id?: number | null;
  51. startDate: DateValue | null;
  52. endDate: DateValue | null;
  53. quality: QualityType;
  54. isExisting: boolean;
  55. isEditing?: boolean;
  56. animatedValue: Animated.Value;
  57. }
  58. interface ExistingVisit {
  59. id: number;
  60. startDate: DateValue;
  61. endDate: DateValue;
  62. quality: QualityType;
  63. }
  64. interface RouteParams {
  65. existingVisits?: ExistingVisit[];
  66. }
  67. interface DatePickerState {
  68. visitId: number;
  69. field: 'startDate' | 'endDate';
  70. }
  71. const EditNmDataScreen = ({ navigation, route }: { navigation: any; route: any }) => {
  72. const id = route.params?.regionId;
  73. const isFromRegionsList = route.params?.regionsList ?? false;
  74. const token = storage.get('token', StoreType.STRING) as string;
  75. const { data: existingVisits } = useGetVisitsQuery(id, token, token ? true : false);
  76. const { mutateAsync: addVisit } = usePostAddVisitMutation();
  77. const { mutateAsync: updateVisitAsync } = usePostUpdateVisitMutation();
  78. const { mutateAsync: deleteVisitAsync } = usePostDeleteVisitMutation();
  79. const { mutateAsync: getRegion } = usePostGetSingleRegionMutation();
  80. const [visits, setVisits] = useState<Visit[]>([]);
  81. const [showDatePicker, setShowDatePicker] = useState<DatePickerState | null>(null);
  82. const [isLoading, setIsLoading] = useState<boolean>(false);
  83. const actionSheetRef = useRef<any>(null);
  84. const [selectedYear, setSelectedYear] = useState<number>(new Date().getFullYear());
  85. const [selectedMonth, setSelectedMonth] = useState<number | null>(null);
  86. const [selectedDay, setSelectedDay] = useState<number | null>(null);
  87. const { userData, setUserData, nmRegions, setNmRegions } = useRegion();
  88. const [modalState, setModalState] = useState({
  89. isWarningVisible: false,
  90. type: 'success',
  91. title: '',
  92. buttonTitle: '',
  93. message: '',
  94. action: () => {}
  95. });
  96. const createEmptyVisit = useCallback(
  97. (): Visit => ({
  98. id: Date.now() + Math.random(),
  99. startDate: null,
  100. endDate: null,
  101. quality: qualityOptions[2],
  102. isExisting: false,
  103. animatedValue: new Animated.Value(0)
  104. }),
  105. []
  106. );
  107. useEffect(() => {
  108. if (existingVisits && existingVisits.data && existingVisits.data?.length > 0) {
  109. const mappedVisits = existingVisits.data.map(
  110. (visit): Visit => ({
  111. ...visit,
  112. isExisting: true,
  113. isEditing: false,
  114. animatedValue: new Animated.Value(1),
  115. startDate: {
  116. year: visit.year_from || null,
  117. month: visit.month_from || null,
  118. day: visit.day_from || null
  119. },
  120. endDate: {
  121. year: visit.year_to || null,
  122. month: visit.month_to || null,
  123. day: visit.day_to || null
  124. },
  125. quality: qualityOptions.find((q) => q.id === visit.quality) || qualityOptions[2]
  126. })
  127. );
  128. setVisits(mappedVisits);
  129. }
  130. }, [existingVisits]);
  131. const renderOption = (name: string) => (
  132. <View style={styles.dropdownOption}>
  133. <Text style={styles.placeholderStyle}>{name}</Text>
  134. </View>
  135. );
  136. const addNewVisit = useCallback((): void => {
  137. const hasEmptyVisit = visits.some(
  138. (visit: Visit) => !visit.startDate || !visit.endDate || !visit.quality
  139. );
  140. if (hasEmptyVisit) {
  141. Alert.alert('Please fill all fields in existing visits before adding a new one.');
  142. return;
  143. }
  144. const newVisit = createEmptyVisit();
  145. setVisits((prev) => [newVisit, ...prev]);
  146. Animated.timing(newVisit.animatedValue, {
  147. toValue: 1,
  148. duration: 300,
  149. easing: Easing.out(Easing.quad),
  150. useNativeDriver: false
  151. }).start();
  152. }, [visits, createEmptyVisit]);
  153. const updateVisit = useCallback((id: number, field: keyof Visit, value: any): void => {
  154. setVisits((prevVisits) =>
  155. prevVisits.map((visit) => (visit.id === id ? { ...visit, [field]: value } : visit))
  156. );
  157. }, []);
  158. const toggleEditVisit = useCallback(
  159. async (visitId: number): Promise<void> => {
  160. const visit = visits.find((v) => v.id === visitId);
  161. if (visit && visit.isEditing) {
  162. if (!compareDates(visit.startDate, visit.endDate)) {
  163. Alert.alert('Start date cannot be after end date.');
  164. return;
  165. }
  166. await updateVisitAsync(
  167. {
  168. token,
  169. region: id,
  170. id: visitId,
  171. quality: visit.quality.id,
  172. year_from: visit.startDate?.year || null,
  173. month_from: visit.startDate?.month || null,
  174. day_from: visit.startDate?.day || null,
  175. year_to: visit.endDate?.year || null,
  176. month_to: visit.endDate?.month || null,
  177. day_to: visit.endDate?.day || null,
  178. // completed: 1,
  179. hidden: 0
  180. },
  181. {
  182. onSuccess: async (data) => {
  183. await getRegion(
  184. { token, id },
  185. {
  186. onSuccess: (res) => {
  187. if (isFromRegionsList && res.region) {
  188. const updatedNM = nmRegions.map((item: NmRegion) => {
  189. if (item.id === id) {
  190. return {
  191. ...item,
  192. year: res.region?.first_visited_in_year,
  193. last: res.region?.last_visited_in_year,
  194. quality: res.region?.best_visit_quality,
  195. visits: res.region?.no_of_visits
  196. };
  197. }
  198. return item;
  199. });
  200. setNmRegions(updatedNM);
  201. return;
  202. }
  203. if (res.region) {
  204. const updatedNM = {
  205. ...userData,
  206. first_visit_year: res.region.first_visited_in_year,
  207. last_visit_year: res.region.last_visited_in_year,
  208. best_visit_quality: res.region.best_visit_quality,
  209. no_of_visits: res.region.no_of_visits,
  210. visited: true
  211. };
  212. setUserData(updatedNM);
  213. }
  214. },
  215. onError: (err) => {
  216. console.log('err', err);
  217. }
  218. }
  219. );
  220. Alert.alert('Success', 'Visit updated successfully!', [
  221. { text: 'OK', onPress: () => navigation.goBack() }
  222. ]);
  223. },
  224. onError: (error) => {
  225. console.log('updateVisitAsync error', error);
  226. }
  227. }
  228. );
  229. }
  230. setVisits((prevVisits) =>
  231. prevVisits.map((visit) =>
  232. visit.id === visitId ? { ...visit, isEditing: !visit.isEditing } : visit
  233. )
  234. );
  235. },
  236. [visits]
  237. );
  238. const deleteVisit = useCallback(
  239. async (visitId: number): Promise<void> => {
  240. const visitToDelete = visits.find((visit: Visit) => visit.id === visitId);
  241. if (!visitToDelete) return;
  242. if (visitToDelete.isExisting) {
  243. setModalState({
  244. type: 'delete',
  245. title: `Delete visit`,
  246. message: `Are you sure you want to delete this visit?`,
  247. action: async () => {
  248. await deleteVisitAsync(
  249. {
  250. token,
  251. id: visitToDelete.id
  252. },
  253. {
  254. onSuccess: async (res) => {
  255. Animated.timing(visitToDelete.animatedValue, {
  256. toValue: 0,
  257. duration: 300,
  258. easing: Easing.in(Easing.quad),
  259. useNativeDriver: false
  260. }).start(() => {
  261. setVisits((prevVisits) => prevVisits.filter((visit) => visit.id !== visitId));
  262. });
  263. getRegion(
  264. { token, id },
  265. {
  266. onSuccess: (res) => {
  267. if (isFromRegionsList && res.not_visited) {
  268. const updatedNM = nmRegions.map((item: NmRegion) => {
  269. if (item.id === id) {
  270. return {
  271. ...item,
  272. year: 0,
  273. last: 0,
  274. quality: 3,
  275. visits: 0
  276. };
  277. }
  278. return item;
  279. });
  280. setNmRegions(updatedNM);
  281. return;
  282. } else if (isFromRegionsList && res.region) {
  283. const updatedNM = nmRegions.map((item: NmRegion) => {
  284. if (item.id === id) {
  285. return {
  286. ...item,
  287. year: res.region?.first_visited_in_year,
  288. last: res.region?.last_visited_in_year,
  289. quality: res.region?.best_visit_quality,
  290. visits: res.region?.no_of_visits
  291. };
  292. }
  293. return item;
  294. });
  295. setNmRegions(updatedNM);
  296. return;
  297. }
  298. if (res.not_visited) {
  299. const updatedNM = {
  300. ...userData,
  301. first_visit_year: 0,
  302. last_visit_year: 0,
  303. best_visit_quality: 3,
  304. no_of_visits: 0,
  305. visited: false
  306. };
  307. setUserData(updatedNM);
  308. return;
  309. }
  310. if (res.region) {
  311. const updatedNM = {
  312. ...userData,
  313. first_visit_year: res.region.first_visited_in_year,
  314. last_visit_year: res.region.last_visited_in_year,
  315. best_visit_quality: res.region.best_visit_quality,
  316. no_of_visits: res.region.no_of_visits,
  317. visited: true
  318. };
  319. setUserData(updatedNM);
  320. }
  321. },
  322. onError: (err) => {
  323. console.log('err', err);
  324. }
  325. }
  326. );
  327. },
  328. onError: (err) => {
  329. console.log('delete err', err);
  330. }
  331. }
  332. );
  333. },
  334. buttonTitle: 'Delete',
  335. isWarningVisible: true
  336. });
  337. } else {
  338. Animated.timing(visitToDelete.animatedValue, {
  339. toValue: 0,
  340. duration: 300,
  341. easing: Easing.in(Easing.quad),
  342. useNativeDriver: false
  343. }).start(() => {
  344. setVisits((prevVisits) => prevVisits.filter((visit) => visit.id !== visitId));
  345. });
  346. }
  347. },
  348. [visits]
  349. );
  350. const formatDateForDisplay = useCallback((dateValue: DateValue | null): string => {
  351. if (!dateValue || !dateValue.year) return 'Select date';
  352. let result = dateValue.year.toString();
  353. if (dateValue.month) {
  354. result = `${dateValue.month.toString().padStart(2, '0')}.${result}`;
  355. if (dateValue.day) {
  356. result = `${dateValue.day.toString().padStart(2, '0')}.${result}`;
  357. }
  358. }
  359. return result;
  360. }, []);
  361. const isDateValid = useCallback((dateValue: DateValue | null): boolean => {
  362. return dateValue !== null && dateValue.year !== null;
  363. }, []);
  364. const compareDates = useCallback(
  365. (startDate: DateValue | null, endDate: DateValue | null): boolean => {
  366. if (!startDate || !endDate || !startDate.year || !endDate.year) return true;
  367. const maxEndDay = moment(`${endDate.year}-${endDate.month ?? 12}`, 'YYYY-M').daysInMonth();
  368. const start = moment({
  369. year: startDate.year,
  370. month: (startDate.month || 1) - 1,
  371. day: startDate.day || 1
  372. });
  373. const end = moment({
  374. year: endDate.year,
  375. month: (endDate.month || 12) - 1,
  376. day: endDate.day || maxEndDay
  377. });
  378. if (!start || !end) return true;
  379. return start.isSameOrBefore(end);
  380. },
  381. []
  382. );
  383. const validateVisits = useCallback((): boolean => {
  384. const newVisits = visits.filter((visit: Visit) => !visit.isExisting);
  385. for (const visit of newVisits) {
  386. if (!isDateValid(visit.startDate) || !isDateValid(visit.endDate) || !visit.quality) {
  387. Alert.alert('Please fill all fields for each visit.');
  388. return false;
  389. }
  390. if (!compareDates(visit.startDate, visit.endDate)) {
  391. Alert.alert('Start date cannot be after end date.');
  392. return false;
  393. }
  394. }
  395. return true;
  396. }, [visits, isDateValid, compareDates]);
  397. const handleSave = useCallback(async (): Promise<void> => {
  398. if (!validateVisits()) return;
  399. setIsLoading(true);
  400. try {
  401. const newVisits = visits.filter((visit: Visit) => !visit.isExisting);
  402. if (newVisits.length === 0) {
  403. // Alert.alert('No new visits to save.');
  404. return;
  405. }
  406. newVisits.forEach(async (v) => {
  407. await addVisit(
  408. {
  409. token,
  410. region: id,
  411. quality: v.quality.id,
  412. year_from: v.startDate?.year || null,
  413. month_from: v.startDate?.month || null,
  414. day_from: v.startDate?.day || null,
  415. year_to: v.endDate?.year || null,
  416. month_to: v.endDate?.month || null,
  417. day_to: v.endDate?.day || null,
  418. // completed: 1,
  419. hidden: 0
  420. },
  421. {
  422. onSuccess: async (data) => {
  423. await getRegion(
  424. { token, id },
  425. {
  426. onSuccess: (res) => {
  427. if (isFromRegionsList && res.region) {
  428. const updatedNM = nmRegions.map((item: NmRegion) => {
  429. if (item.id === id) {
  430. return {
  431. ...item,
  432. year: res.region?.first_visited_in_year,
  433. last: res.region?.last_visited_in_year,
  434. quality: res.region?.best_visit_quality,
  435. visits: res.region?.no_of_visits
  436. };
  437. }
  438. return item;
  439. });
  440. setNmRegions(updatedNM);
  441. return;
  442. }
  443. if (res.region) {
  444. const updatedNM = {
  445. ...userData,
  446. first_visit_year: res.region.first_visited_in_year,
  447. last_visit_year: res.region.last_visited_in_year,
  448. best_visit_quality: res.region.best_visit_quality,
  449. no_of_visits: res.region.no_of_visits,
  450. visited: true
  451. };
  452. setUserData(updatedNM);
  453. }
  454. },
  455. onError: (err) => {
  456. console.log('err', err);
  457. }
  458. }
  459. );
  460. },
  461. onError: (error) => {
  462. console.log('addVisit error', error);
  463. }
  464. }
  465. );
  466. });
  467. Alert.alert('Success', 'Visits saved successfully!', [
  468. { text: 'OK', onPress: () => navigation.goBack() }
  469. ]);
  470. } catch (error) {
  471. console.log('Error saving visits:', error);
  472. } finally {
  473. setIsLoading(false);
  474. }
  475. }, [validateVisits, visits, navigation]);
  476. const currentYear = new Date().getFullYear();
  477. const years = Array.from({ length: 120 }, (_, i) => currentYear - 80 + i);
  478. const getAvailableMonths = (year: number) => {
  479. const allMonths = [
  480. { label: '-', value: null },
  481. { label: 'Jan', value: 1 },
  482. { label: 'Feb', value: 2 },
  483. { label: 'Mar', value: 3 },
  484. { label: 'Apr', value: 4 },
  485. { label: 'May', value: 5 },
  486. { label: 'Jun', value: 6 },
  487. { label: 'Jul', value: 7 },
  488. { label: 'Aug', value: 8 },
  489. { label: 'Sep', value: 9 },
  490. { label: 'Oct', value: 10 },
  491. { label: 'Nov', value: 11 },
  492. { label: 'Dec', value: 12 }
  493. ];
  494. return allMonths;
  495. };
  496. const months = getAvailableMonths(selectedYear);
  497. const getDaysInMonth = (
  498. year: number,
  499. month: number | null
  500. ): Array<{ label: string; value: number | null }> => {
  501. if (!month) return [{ label: '-', value: null }];
  502. const daysCount = moment(`${year}-${month}`, 'YYYY-M').daysInMonth();
  503. const days = [{ label: '-', value: null }];
  504. for (let i = 1; i <= daysCount; i++) {
  505. days.push({ label: i.toString(), value: i as never });
  506. }
  507. return days;
  508. };
  509. const days = getDaysInMonth(selectedYear, selectedMonth);
  510. const openDatePicker = (
  511. visitId: number,
  512. field: 'startDate' | 'endDate',
  513. initialDate?: DateValue | null
  514. ) => {
  515. setShowDatePicker({ visitId, field });
  516. if (initialDate && initialDate.year) {
  517. setSelectedYear(initialDate.year);
  518. setSelectedMonth(initialDate.month || null);
  519. setSelectedDay(initialDate.day || null);
  520. } else {
  521. const today = new Date();
  522. setSelectedYear(today.getFullYear());
  523. setSelectedMonth(today.getMonth() + 1);
  524. setSelectedDay(today.getDate());
  525. }
  526. actionSheetRef.current?.show();
  527. };
  528. const handleDateConfirm = () => {
  529. if (showDatePicker) {
  530. const dateValue: DateValue = {
  531. year: selectedYear,
  532. month: selectedMonth,
  533. day: selectedDay
  534. };
  535. updateVisit(showDatePicker.visitId, showDatePicker.field, dateValue);
  536. setShowDatePicker(null);
  537. actionSheetRef.current?.hide();
  538. }
  539. };
  540. const handleDateCancel = () => {
  541. setShowDatePicker(null);
  542. actionSheetRef.current?.hide();
  543. };
  544. const renderDatePicker = useCallback(
  545. (
  546. visitId: number,
  547. field: 'startDate' | 'endDate',
  548. currentDate: DateValue | null
  549. ): JSX.Element => (
  550. <TouchableOpacity
  551. style={styles.dateInput}
  552. onPress={() => openDatePicker(visitId, field, currentDate)}
  553. >
  554. <Text style={[styles.dateText, !isDateValid(currentDate) && styles.placeholderText]}>
  555. {formatDateForDisplay(currentDate)}
  556. </Text>
  557. <MaterialCommunityIcons name="chevron-down" size={20} color="#666" />
  558. </TouchableOpacity>
  559. ),
  560. [formatDateForDisplay, isDateValid]
  561. );
  562. const renderVisitItem = useCallback(
  563. (visit: Visit, index: number): JSX.Element => {
  564. const isEditable = !visit.isExisting || visit.isEditing;
  565. return (
  566. <Animated.View
  567. key={visit.id}
  568. style={[
  569. styles.visitItem,
  570. !isEditable && styles.existingVisitItem,
  571. {
  572. // opacity: visit.animatedValue
  573. }
  574. ]}
  575. >
  576. <View style={styles.visitContent}>
  577. {visit.trip_id ? <TripIcon width={18} height={18} fill={Colors.DARK_BLUE} /> : null}
  578. <View style={{ flex: 1, gap: 12 }}>
  579. <View style={styles.dateRow}>
  580. <View style={styles.column}>
  581. <Text style={styles.label}>Start of visit</Text>
  582. {isEditable ? (
  583. renderDatePicker(visit.id, 'startDate', visit.startDate)
  584. ) : (
  585. <View style={[styles.dateInput]}>
  586. <Text style={styles.dateText}>{formatDateForDisplay(visit.startDate)}</Text>
  587. </View>
  588. )}
  589. </View>
  590. <View style={styles.column}>
  591. <Text style={styles.label}>End of visit</Text>
  592. {isEditable ? (
  593. renderDatePicker(visit.id, 'endDate', visit.endDate)
  594. ) : (
  595. <View style={[styles.dateInput]}>
  596. <Text style={styles.dateText}>{formatDateForDisplay(visit.endDate)}</Text>
  597. </View>
  598. )}
  599. </View>
  600. </View>
  601. <View style={styles.qualityRow}>
  602. <View style={styles.qualityColumn}>
  603. <Text style={styles.label}>Quality</Text>
  604. {isEditable ? (
  605. <Dropdown
  606. style={styles.dropdown}
  607. placeholderStyle={styles.placeholderStyle}
  608. containerStyle={{ borderRadius: 4 }}
  609. selectedTextStyle={styles.placeholderStyle}
  610. data={qualityOptions}
  611. labelField="name"
  612. valueField="id"
  613. value={visit.quality.id}
  614. placeholder="Best visit quality"
  615. onChange={(item) =>
  616. updateVisit(visit.id, 'quality', { id: item.id, name: item.name })
  617. }
  618. renderItem={(item) => renderOption(item.name)}
  619. />
  620. ) : (
  621. <View style={[styles.dropdown, styles.readOnlyInput]}>
  622. <Text style={styles.placeholderStyle}>{visit.quality.name}</Text>
  623. </View>
  624. )}
  625. </View>
  626. </View>
  627. </View>
  628. <View style={styles.actionButtons}>
  629. {visit.isExisting && (
  630. <TouchableOpacity
  631. style={[styles.editButton, visit.isEditing && styles.saveEditButton]}
  632. onPress={() => toggleEditVisit(visit.id)}
  633. >
  634. {visit.isEditing ? (
  635. <MaterialCommunityIcons name={'check'} size={16} color={Colors.WHITE} />
  636. ) : (
  637. <EditSvg width={14} height={14} />
  638. )}
  639. </TouchableOpacity>
  640. )}
  641. <TouchableOpacity style={styles.deleteButton} onPress={() => deleteVisit(visit.id)}>
  642. <TrashSVG height={16} fill={Colors.WHITE} />
  643. </TouchableOpacity>
  644. </View>
  645. </View>
  646. </Animated.View>
  647. );
  648. },
  649. [
  650. renderDatePicker,
  651. deleteVisit,
  652. toggleEditVisit,
  653. updateVisit,
  654. formatDateForDisplay,
  655. renderOption,
  656. visits
  657. ]
  658. );
  659. return (
  660. <PageWrapper>
  661. <Header label={'Add visits'} />
  662. <ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false}>
  663. <TouchableOpacity style={styles.addRegionBtn} onPress={addNewVisit}>
  664. <MaterialCommunityIcons name="plus-circle" size={20} color={Colors.DARK_BLUE} />
  665. <Text style={styles.addRegionBtntext}>Add new visit</Text>
  666. </TouchableOpacity>
  667. {visits.map(renderVisitItem)}
  668. </ScrollView>
  669. <View style={[styles.buttonContainer, { marginBottom: 20 }]}>
  670. <Button onPress={handleSave} disabled={isLoading}>
  671. {isLoading ? 'Saving...' : 'Save'}
  672. </Button>
  673. <Button
  674. variant={ButtonVariants.OPACITY}
  675. containerStyles={{
  676. backgroundColor: Colors.WHITE,
  677. borderColor: Colors.BORDER_LIGHT
  678. }}
  679. textStyles={{ color: Colors.DARK_BLUE }}
  680. onPress={() => navigation.goBack()}
  681. >
  682. Close
  683. </Button>
  684. </View>
  685. <ActionSheet
  686. ref={actionSheetRef}
  687. gestureEnabled={false}
  688. headerAlwaysVisible={true}
  689. onTouchBackdrop={handleDateConfirm}
  690. CustomHeaderComponent={
  691. <View style={styles.datePickerHeader}>
  692. <TouchableOpacity onPress={handleDateCancel}>
  693. <Text style={styles.datePickerCancel}>Cancel</Text>
  694. </TouchableOpacity>
  695. <Text style={styles.datePickerTitle}>Select Date</Text>
  696. <TouchableOpacity onPress={handleDateConfirm}>
  697. <Text style={styles.datePickerConfirm}>Done</Text>
  698. </TouchableOpacity>
  699. </View>
  700. }
  701. >
  702. <View style={styles.wheelContainer}>
  703. <View style={styles.wheelColumn}>
  704. <Text style={styles.wheelLabel}>Day</Text>
  705. <WheelPicker
  706. style={styles.wheelPicker}
  707. textColor={Colors.DARK_BLUE}
  708. itemStyle={{ fontSize: 16, fontFamily: 'montserrat-600', padding: 0 }}
  709. pickerData={days?.map((d) => d.label)}
  710. selectedValue={days?.find((d) => d.value === selectedDay)?.label || '-'}
  711. onValueChange={(value: string) => {
  712. const day = days?.find((d) => d.label === value);
  713. setSelectedDay(day?.value || null);
  714. }}
  715. />
  716. </View>
  717. <View style={styles.wheelColumn}>
  718. <Text style={styles.wheelLabel}>Month</Text>
  719. <WheelPicker
  720. style={styles.wheelPicker}
  721. textColor={Colors.DARK_BLUE}
  722. itemStyle={{
  723. fontSize: 16,
  724. fontFamily: 'montserrat-600'
  725. }}
  726. pickerData={months ? months?.map((m) => m.label) : []}
  727. selectedValue={months?.find((m) => m.value === selectedMonth)?.label || '-'}
  728. onValueChange={(value: string) => {
  729. const month = months?.find((m) => m.label === value);
  730. setSelectedMonth(month?.value || null);
  731. if (!month?.value) {
  732. setSelectedDay(null);
  733. }
  734. }}
  735. />
  736. </View>
  737. <View style={styles.wheelColumn}>
  738. <Text style={styles.wheelLabel}>Year</Text>
  739. <WheelPicker
  740. style={styles.wheelPicker}
  741. textColor={Colors.DARK_BLUE}
  742. itemStyle={{ fontSize: 16, fontFamily: 'montserrat-600' }}
  743. isCyclic={true}
  744. pickerData={years}
  745. selectedValue={selectedYear.toString()}
  746. onValueChange={(value: number) => {
  747. setSelectedYear(value);
  748. }}
  749. />
  750. </View>
  751. </View>
  752. </ActionSheet>
  753. <WarningModal
  754. type={modalState.type}
  755. isVisible={modalState.isWarningVisible}
  756. buttonTitle={modalState.buttonTitle}
  757. message={modalState.message}
  758. action={modalState.action}
  759. onClose={() => setModalState({ ...modalState, isWarningVisible: false })}
  760. title={modalState.title}
  761. />
  762. </PageWrapper>
  763. );
  764. };
  765. const styles = StyleSheet.create({
  766. scrollView: {
  767. flex: 1
  768. },
  769. visitItem: {
  770. backgroundColor: Colors.WHITE,
  771. borderRadius: 8,
  772. padding: 12,
  773. marginBottom: 12,
  774. borderWidth: 0.5,
  775. borderColor: Colors.LIGHT_GRAY
  776. },
  777. existingVisitItem: {},
  778. visitContent: {
  779. gap: 12,
  780. flexDirection: 'row',
  781. justifyContent: 'space-between',
  782. alignItems: 'center'
  783. },
  784. dateRow: {
  785. flexDirection: 'row',
  786. alignItems: 'flex-start',
  787. gap: 12
  788. },
  789. qualityRow: {
  790. flexDirection: 'row'
  791. },
  792. qualityColumn: {
  793. flex: 1
  794. },
  795. column: {
  796. flex: 1
  797. },
  798. actionButtons: {
  799. flexDirection: 'column',
  800. alignItems: 'center',
  801. gap: 8
  802. },
  803. editButton: {
  804. width: 30,
  805. height: 30,
  806. borderRadius: 15,
  807. justifyContent: 'center',
  808. alignItems: 'center',
  809. borderWidth: 1,
  810. borderColor: Colors.LIGHT_GRAY
  811. },
  812. saveEditButton: {
  813. borderColor: Colors.ORANGE,
  814. backgroundColor: Colors.ORANGE
  815. },
  816. label: {
  817. fontSize: getFontSize(12),
  818. color: Colors.DARK_BLUE,
  819. marginBottom: 4,
  820. fontWeight: '600'
  821. },
  822. dateInput: {
  823. borderRadius: 6,
  824. paddingHorizontal: 8,
  825. paddingVertical: 6,
  826. flexDirection: 'row',
  827. alignItems: 'center',
  828. justifyContent: 'space-between',
  829. backgroundColor: Colors.FILL_LIGHT,
  830. height: 36
  831. },
  832. readOnlyInput: {
  833. justifyContent: 'center'
  834. },
  835. dateText: {
  836. fontSize: getFontSize(12),
  837. color: Colors.DARK_BLUE,
  838. fontWeight: '500'
  839. },
  840. placeholderText: {
  841. color: Colors.TEXT_GRAY,
  842. flexWrap: 'wrap'
  843. },
  844. deleteButton: {
  845. width: 30,
  846. height: 30,
  847. borderRadius: 15,
  848. backgroundColor: Colors.RED,
  849. alignItems: 'center',
  850. justifyContent: 'center'
  851. },
  852. buttonContainer: {
  853. marginBottom: 32,
  854. gap: 12
  855. },
  856. datePickerHeader: {
  857. flexDirection: 'row',
  858. justifyContent: 'space-between',
  859. alignItems: 'center',
  860. paddingHorizontal: 20,
  861. paddingVertical: 16,
  862. borderBottomWidth: 1,
  863. borderBottomColor: '#eee'
  864. },
  865. datePickerTitle: {
  866. fontSize: 18,
  867. fontWeight: '600',
  868. color: Colors.DARK_BLUE
  869. },
  870. datePickerCancel: {
  871. fontSize: 16,
  872. color: '#666'
  873. },
  874. datePickerConfirm: {
  875. fontSize: 16,
  876. color: Colors.DARK_BLUE,
  877. fontWeight: '600'
  878. },
  879. wheelContainer: {
  880. flexDirection: 'row',
  881. paddingHorizontal: 14,
  882. paddingTop: 16,
  883. paddingBottom: 24
  884. },
  885. wheelColumn: {
  886. flex: 1,
  887. alignItems: 'center'
  888. },
  889. wheelLabel: {
  890. fontSize: 14,
  891. color: Colors.DARK_BLUE,
  892. marginBottom: 8,
  893. fontWeight: '600'
  894. },
  895. wheelPicker: {
  896. height: 210,
  897. width: '100%',
  898. backgroundColor: 'white'
  899. },
  900. dropdown: {
  901. height: 36,
  902. backgroundColor: '#F4F4F4',
  903. borderRadius: 4,
  904. paddingHorizontal: 8
  905. },
  906. dropdownOption: {
  907. paddingVertical: 12,
  908. paddingHorizontal: 16
  909. },
  910. placeholderStyle: {
  911. fontSize: 12,
  912. color: Colors.DARK_BLUE,
  913. fontWeight: '500'
  914. },
  915. addRegionBtn: {
  916. display: 'flex',
  917. justifyContent: 'center',
  918. alignItems: 'center',
  919. flexDirection: 'row',
  920. borderRadius: 4,
  921. gap: 10,
  922. padding: 10,
  923. borderColor: Colors.DARK_BLUE,
  924. borderWidth: 1,
  925. borderStyle: 'solid',
  926. marginBottom: 12
  927. },
  928. addRegionBtntext: {
  929. color: Colors.DARK_BLUE,
  930. fontSize: getFontSize(14),
  931. fontFamily: 'redhat-700'
  932. }
  933. });
  934. export default EditNmDataScreen;