From e2a9d3acace055de206f21d6124b0e3868a3235e Mon Sep 17 00:00:00 2001 From: K-Owiti Date: Sun, 30 Aug 2026 22:19:51 +0300 Subject: [PATCH 1/2] EditPointData functionality --- .../insecticideResistanceBioassays.entity.ts | 10 +- .../insecticideResistance.resolver.ts | 8 + .../insecticideResistance.service.ts | 3 + .../src/db/occurrence/occurrence.service.ts | 91 ++- src/UI/api/api.ts | 8 +- .../editPointData/BionomicsCard.tsx | 204 +++++++ .../InsecticideResistanceCard.tsx | 143 +++++ .../editPointData/OccurrenceCard.tsx | 264 +++++++++ src/UI/components/shared/useReferenceDb.ts | 60 ++ src/UI/pages/editPointData.tsx | 548 ++++++++---------- 10 files changed, 982 insertions(+), 357 deletions(-) create mode 100644 src/UI/components/editPointData/BionomicsCard.tsx create mode 100644 src/UI/components/editPointData/InsecticideResistanceCard.tsx create mode 100644 src/UI/components/editPointData/OccurrenceCard.tsx create mode 100644 src/UI/components/shared/useReferenceDb.ts diff --git a/src/API/src/db/insecticideResistance/entities/insecticideResistanceBioassays.entity.ts b/src/API/src/db/insecticideResistance/entities/insecticideResistanceBioassays.entity.ts index a4c75d039..4d3ce08a2 100644 --- a/src/API/src/db/insecticideResistance/entities/insecticideResistanceBioassays.entity.ts +++ b/src/API/src/db/insecticideResistance/entities/insecticideResistanceBioassays.entity.ts @@ -514,8 +514,12 @@ export class InsecticideResistanceBioassays extends BaseEntity { ) ace1GenotypeFrequenciesFormally119: Ace1GenotypeFrequenciesFormally119; - @OneToMany(() => Occurrence, (occurrence) => occurrence.sample, { - onDelete: 'CASCADE', - }) + @OneToMany( + () => Occurrence, + (occurrence) => occurrence.insecticideResistanceBioassays, + { + onDelete: 'CASCADE', + }, + ) occurrence: Occurrence; } diff --git a/src/API/src/db/insecticideResistance/insecticideResistance.resolver.ts b/src/API/src/db/insecticideResistance/insecticideResistance.resolver.ts index 02107468a..f4d8013e8 100644 --- a/src/API/src/db/insecticideResistance/insecticideResistance.resolver.ts +++ b/src/API/src/db/insecticideResistance/insecticideResistance.resolver.ts @@ -10,4 +10,12 @@ export class InsecticideResistanceResolver { constructor( private insecticideResistanceService: InsecticideResistanceService, ) {} + @Query(insecticideResistanceTypeResolver, { + name: 'insecticideResistanceById', + }) + async getInsecticideResistanceById( + @Args('id', { type: () => String }) id: string, + ) { + return this.insecticideResistanceService.findOneById(id); + } } diff --git a/src/API/src/db/insecticideResistance/insecticideResistance.service.ts b/src/API/src/db/insecticideResistance/insecticideResistance.service.ts index ec7355aa8..052dbd870 100644 --- a/src/API/src/db/insecticideResistance/insecticideResistance.service.ts +++ b/src/API/src/db/insecticideResistance/insecticideResistance.service.ts @@ -9,4 +9,7 @@ export class InsecticideResistanceService { @InjectRepository(InsecticideResistanceBioassays) private insecticideResistanceRepository: Repository, ) {} + findOneById(id: string): Promise { + return this.insecticideResistanceRepository.findOne({ where: { id: id } }); + } } diff --git a/src/API/src/db/occurrence/occurrence.service.ts b/src/API/src/db/occurrence/occurrence.service.ts index 030ee5850..454db1bc1 100644 --- a/src/API/src/db/occurrence/occurrence.service.ts +++ b/src/API/src/db/occurrence/occurrence.service.ts @@ -720,52 +720,12 @@ export class OccurrenceService { where: { id: occurrenceId }, relations: [ 'recordedSpecies', - 'Larval_site', - 'ace1AlleleFrequencies', - 'ace1GenotypeFrequencies', - 'ace1MethodAndSample', - 'anthropo_zoophagic', - 'biology', - 'bionomics', - 'biting_activity', - 'biting_rate', - 'cyp4j5AlleleFrequencies', - 'cyp4j5GenotypeFrequencies', - 'cyp6aapAlleleFrequencies', - 'cyp6aapGenotypeFrequencies', - 'cyp6p4AlleleFrequencies', - 'cyp6p4GenotypeFrequencies', - 'cytochromesP450_cypMethodAndSample', - 'dataset', - 'endo_exophagic', - 'endo_exophily', - 'environment', - 'genotypicRepresentativeness', - 'geography_columns', - 'geometry_columns', - 'gste2_114AlleleFrequencies', - 'gste2_114GenotypeFrequencies', - 'gste2_119AlleleFrequencies', - 'gste2_119GenotypeFrequencies', - 'gsteMethodAndSample', - 'infection', - 'insecticideResistanceBioassays', - 'kdrGenotypeFrequencies', - 'occurrence', - 'rdl296AlleleFrequencies', - 'rdl296GenotypeFrequencies', - 'rdlMethodAndSample', - 'recorded_species', 'reference', 'sample', 'site', - 'species_information', - 'uploaded_dataset', - 'uploaded_dataset_log', - 'user_role', - 'vgsc1570AlleleFrequencies', - 'vgsc1570GenotypeFrequencies', - 'vgsc402AlleleFrequencies', + 'dataset', + 'bionomics', + 'insecticideResistanceBioassays', ], }); if (!record) throw new NotFoundException('Occurrence not found'); @@ -782,15 +742,23 @@ export class OccurrenceService { where: { occurrence: { id: occurrenceId } }, }); - case 'insecticideResistanceBioassays': - return this.insecticideResistanceBioassaysRepository.find({ - where: { occurrence: { id: occurrenceId } }, + case 'insecticideResistanceBioassays': { + const occ = await this.occurrenceRepository.findOne({ + where: { id: occurrenceId }, + relations: ['insecticideResistanceBioassays'], }); + return occ?.insecticideResistanceBioassays + ? [occ.insecticideResistanceBioassays] + : []; + } - case 'bionomics': - return this.bionomicsRepository.find({ - where: { occurrence: { id: occurrenceId } }, + case 'bionomics': { + const occ = await this.occurrenceRepository.findOne({ + where: { id: occurrenceId }, + relations: ['bionomics'], }); + return occ?.bionomics ? [occ.bionomics] : []; + } case 'ace1AlleleFrequencies': { const records = await this.ace1AlleleFrequenciesRepository.find({ @@ -1278,8 +1246,31 @@ export class OccurrenceService { } async getPointDataBySource(source_id: string): Promise { + // Detects what kind of input the user pasted + const isUuid = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test( + source_id, + ); + const isNum = /^\d+$/.test(source_id); + + // Dynamic OR conditions + const whereConditions: any[] = [ + { source_id: source_id }, + ]; + + if (isUuid) { + // If it looks like a UUID, also check the true Reference relation ID + whereConditions.push({ reference: { id: source_id } }); + } + + if (isNum) { + // If it looks like a number, also check the Reference num_id + whereConditions.push({ reference: { num_id: parseInt(source_id, 10) } }); + } + + // Execute the query const records = await this.occurrenceRepository.find({ - where: { source_id }, + where: whereConditions, relations: ['sample', 'reference', 'recordedSpecies', 'site', 'dataset'], }); diff --git a/src/UI/api/api.ts b/src/UI/api/api.ts index da19077c3..d6d9b96c1 100644 --- a/src/UI/api/api.ts +++ b/src/UI/api/api.ts @@ -75,21 +75,21 @@ export const getPointData = async ( occurrenceId: string ) => { const res = await axios.get( - `${apiUrl}/occurrence/getPointData/${entityType}/${occurrenceId}` + `${apiUrl}occurrence/getPointData/${entityType}/${occurrenceId}` ); return res.data; }; export const getPointDataBySource = async (sourceId: string) => { const res = await axios.get( - `${apiUrl}/occurrence/getPointDataBySource/${sourceId}` + `${apiUrl}occurrence/getPointDataBySource/${sourceId}` ); return res.data; }; export const getAllEditLogs = async () => { try { - const res = await axios.get(`${apiUrl}/edit-logs/getAllLogs`); + const res = await axios.get(`${apiUrl}edit-logs/getAllLogs`); return res.data; } catch (error) { console.error('Failed to fetch edit logs:', error); @@ -103,7 +103,7 @@ export const modifyFullPointData = async ( currentUser: any, reasonForEdit: any ) => { - const res = await axios.post(`${apiUrl}/occurrence/modifyFullPointData`, { + const res = await axios.post(`${apiUrl}occurrence/modifyFullPointData`, { body: data, entityType: entityType, editor: currentUser, diff --git a/src/UI/components/editPointData/BionomicsCard.tsx b/src/UI/components/editPointData/BionomicsCard.tsx new file mode 100644 index 000000000..3e461dbad --- /dev/null +++ b/src/UI/components/editPointData/BionomicsCard.tsx @@ -0,0 +1,204 @@ +import React from 'react'; +import { + Box, + Typography, + TextField, + Grid, + Divider, + FormControl, + InputLabel, + Select, + MenuItem, +} from '@mui/material'; + +interface BionomicsCardProps { + data: any; + onChange: ( + e: React.ChangeEvent< + HTMLInputElement | HTMLTextAreaElement | { name?: string; value: unknown } + > + ) => void; +} + +const BionomicsCard: React.FC = ({ data, onChange }) => { + if (!data) return null; + + if (Object.keys(data).length === 0) { + return ( + + + There is no bionomics data available for the following occurrence. + + + ); + } + + // Helper for standard text and number fields + const renderField = (key: string, label: string, type: string = 'text') => ( + + + + ); + + + const renderBoolean = ( + key: string, + label: string, + disabled: boolean = false + ) => ( + + + {label} + + + + ); + + return ( + + {/* SECTION 1: Sampling & Study Design */} + + Sampling & Study Design + + + + {/* Locked down fields passed with 'true' */} + {renderBoolean('adult_data', 'Adult Data Collected?', true)} + {renderBoolean('larval_site_data', 'Larval Site Data Collected?', true)} + {renderField('study_sampling_design', 'Study Sampling Design')} + {renderBoolean('contact_authors', 'Authors Contacted?', true)} + {renderField('contact_notes', 'Contact Notes')} + {renderField('secondary_info', 'Secondary Info')} + + + {/* SECTION 2: Time & Seasonality */} + + Time & Seasonality + + + + {renderField('month_start', 'Month Start', 'number')} + {renderField('year_start', 'Year Start', 'number')} + {renderField('month_end', 'Month End', 'number')} + {renderField('year_end', 'Year End', 'number')} + {renderField('season_given', 'Season (Given)')} + {renderField('season_calc', 'Season (Calculated)')} + {renderField('rainfall_time', 'Rainfall Time')} + {renderField('season_notes', 'Season Notes')} + + + {/* SECTION 3: Interventions & Control */} + + Interventions & Control + + + + {renderBoolean('insecticide_control', 'Insecticide Control Used?')} + {renderBoolean('itn_use', 'ITN Use?', true)} + {renderField('control', 'Control Details')} + + + + + Insecticide Resistance Data Type + + + + + + + + + + + {/* SECTION 4: Data Curation */} + + Data Curation (Admin) + + + + {renderField('data_abstracted_by', 'Abstracted By')} + {renderField('data_checked_by', 'Checked By')} + {renderField('final_check_by', 'Final Check By')} + + + ); +}; + +export default BionomicsCard; diff --git a/src/UI/components/editPointData/InsecticideResistanceCard.tsx b/src/UI/components/editPointData/InsecticideResistanceCard.tsx new file mode 100644 index 000000000..8241b4cd8 --- /dev/null +++ b/src/UI/components/editPointData/InsecticideResistanceCard.tsx @@ -0,0 +1,143 @@ +import React from 'react'; +import { Box, Typography, TextField, Grid, Divider } from '@mui/material'; + +interface IRCardProps { + data: any; + onChange: ( + e: React.ChangeEvent + ) => void; +} + +const InsecticideResistanceCard: React.FC = ({ + data, + onChange, +}) => { + if (!data) return null; + + if (Object.keys(data).length === 0) { + return ( + + + There is no insecticide resistance data available for the following + occurrence. + + + ); + } + + // Helper to render standard text/number fields + const renderField = (key: string, label: string, type: string = 'text') => ( + + + + ); + + return ( + + {/* SECTION 1: Test Details & Protocol */} + + Test Details & Protocol + + + + {renderField('test_protocol', 'Test Protocol')} + {renderField('insecticide_tested', 'Insecticide Tested')} + {renderField('insecticide_class', 'Insecticide Class')} + {renderField('concentration_percent', 'Concentration (%)', 'number')} + {renderField('exposure_period_min', 'Exposure Period (min)', 'number')} + {renderField('generation', 'Mosquito Generation')} + {renderField('wild_caught_larvae_or_adults', 'Wild Caught State')} + {renderField('lower_age_days', 'Lower Age (Days)', 'number')} + {renderField('upper_age_days', 'Upper Age (Days)', 'number')} + + + {/* SECTION 2: Mortality & Knockdown Metrics */} + + Mortality & Knockdown Results + + + + {renderField('mosquitoes_tested_n', 'Mosquitoes Tested (n)', 'number')} + {renderField('mosquitoes_dead_n', 'Mosquitoes Dead (n)', 'number')} + {renderField('percent_mortality', 'Mortality (%)', 'number')} + {renderField( + 'knock_down_exposure_time_min', + 'Knockdown Exposure (min)', + 'number' + )} + {renderField('mosquitoes_knocked_down_n', 'Knocked Down (n)', 'number')} + {renderField('knock_down_percent', 'Knockdown (%)', 'number')} + {renderField('kdt_50_percent_min', 'KDT 50% (min)', 'number')} + {renderField('kdt_90_percent_min', 'KDT 90% (min)', 'number')} + {renderField('kdt_95_percent_min', 'KDT 95% (min)', 'number')} + + + {/* SECTION 3: Synergist Data */} + + Synergist Data + + + + {renderField('synergist_tested', 'Synergist Tested')} + {renderField( + 'synergist_concentration', + 'Synergist Concentration', + 'number' + )} + {renderField('synergist_concentration_unit', 'Concentration Unit')} + + + {/* SECTION 4: Notes */} + + Additional Information + + + + + + + ); +}; + +export default InsecticideResistanceCard; diff --git a/src/UI/components/editPointData/OccurrenceCard.tsx b/src/UI/components/editPointData/OccurrenceCard.tsx new file mode 100644 index 000000000..ffd04855e --- /dev/null +++ b/src/UI/components/editPointData/OccurrenceCard.tsx @@ -0,0 +1,264 @@ +import React from 'react'; +import { + Box, + Typography, + TextField, + FormControl, + InputLabel, + Select, + MenuItem, + Autocomplete, +} from '@mui/material'; + +// Helper functions kept local to the card +const isPrimitive = (val: unknown): val is string | number | boolean | null => + typeof val === 'string' || + typeof val === 'number' || + typeof val === 'boolean' || + val === null; + +const isBoolean = (val: unknown): val is boolean => typeof val === 'boolean'; + +interface OccurrenceCardProps { + data: any; + onChange: (e: any, index?: number) => void; + speciesList: any[]; + referenceList: any[]; +} + +const OccurrenceCard: React.FC = ({ + data, + onChange, + speciesList, + referenceList, +}) => { + if (!data) return null; + + const renderField = (key: string, value: unknown, index?: number) => { + if (key === 'id' || key === 'dec_id' || key === 'source_id') return null; + + // Custom Species Dropdown + if (key === 'recordedSpecies' || key === 'recorded_species') { + const currentId = + typeof value === 'object' && value !== null ? (value as any).id : value; + const currentSpecies = + speciesList.find((s) => s.id === currentId) || null; + + return ( + + option.display_name || option.species || 'Unknown' + } + value={currentSpecies} + onChange={(_, newValue) => { + onChange( + { + target: { + name: key, + value: newValue + ? { id: newValue.id, display_name: newValue.display_name } + : null, + }, + }, + index + ); + }} + renderInput={(params) => ( + + )} + /> + ); + } + + // Custom Reference Dropdown + if (key === 'reference') { + const currentId = + typeof value === 'object' && value !== null ? (value as any).id : value; + + const currentReference = + referenceList?.find((r) => r.id === currentId) || + (typeof value === 'object' && value !== null ? value : null); + + return ( + + option.article_title || + option.citation || + `Unknown (ID: ${option.id})` + } + isOptionEqualToValue={(option, val) => option.id === val.id} + value={currentReference} + onChange={(_, newValue) => { + onChange( + { + target: { + name: key, + value: newValue + ? { + id: newValue.id, + article_title: newValue.article_title, + citation: newValue.citation, + } + : null, + }, + }, + index + ); + }} + renderInput={(params) => ( + + )} + /> + ); + } + + // Read Only Relations + if (key === 'site' || key === 'dataset') { + const linkId = + typeof value === 'object' && value !== null + ? (value as any).id + : String(value || 'None'); + return ( + + ); + } + + // Insecticide Resistance Data Type Dropdown + if (key === 'insecticide_resistance_data') { + return ( + + + Insecticide Resistance Data Type + + + + ); + } + + // Primitive Fields + if (!isPrimitive(value)) return null; + + if ( + isBoolean(value) || + (typeof value === 'string' && + (value.toLowerCase() === 'true' || value.toLowerCase() === 'false')) + ) { + return ( + + {key} + + + ); + } + + return ( + onChange(e, index)} + sx={{ bgcolor: 'white' }} + /> + ); + }; + + return ( + + {Array.isArray(data) ? ( + data.map((record, index) => ( + + + Occurrence Record {index + 1} + + {Object.entries(record).map(([key, value]) => + renderField(key, value, index) + )} + + )) + ) : ( + + + Occurrence Record + + {Object.entries(data).map(([key, value]) => renderField(key, value))} + + )} + + ); +}; + +export default OccurrenceCard; diff --git a/src/UI/components/shared/useReferenceDb.ts b/src/UI/components/shared/useReferenceDb.ts new file mode 100644 index 000000000..23e712931 --- /dev/null +++ b/src/UI/components/shared/useReferenceDb.ts @@ -0,0 +1,60 @@ +import { useState, useEffect } from 'react'; +import { useSelector } from 'react-redux'; +import { AppState } from '../../state/store'; + +let cachedDbOptions: any[] | null = null; +let listeners: any[] = []; + +export const useReferenceDb = (isEnabled: boolean = true) => { + const [data, setData] = useState(cachedDbOptions || []); + const token = useSelector((state: AppState) => state.auth.token); + + useEffect(() => { + if (!isEnabled) return; + if (cachedDbOptions) { + setData(cachedDbOptions); + return; + } + + listeners.push(setData); + + if (listeners.length === 1) { + fetch('/vector-api/graphql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }, + body: JSON.stringify({ + query: ` + query GetAllReferences { + allReferenceData(take: 100, skip: 0, order: "ASC") { + items { + id + author + article_title + citation + year + } + } + } + `, + }), + }) + .then((res) => res.json()) + .then((json) => { + const records = json.data?.allReferenceData?.items || []; + cachedDbOptions = records; + listeners.forEach((l) => l(records)); + listeners = []; + }) + .catch((err) => { + console.error('REFERENCE FETCH ERROR:', err); + listeners.forEach((l) => l([])); + listeners = []; + }); + } + }, [isEnabled, token]); + + return data; +}; diff --git a/src/UI/pages/editPointData.tsx b/src/UI/pages/editPointData.tsx index 112d68cbc..5dc9afaaa 100644 --- a/src/UI/pages/editPointData.tsx +++ b/src/UI/pages/editPointData.tsx @@ -5,12 +5,11 @@ import { Box, Button, CircularProgress, - Autocomplete, - Select, - MenuItem, - FormControl, - InputLabel, Container, + Tabs, + Tab, + Card, + CardContent, } from '@mui/material'; import { getPointData, @@ -25,141 +24,139 @@ import { useSelector } from 'react-redux'; import { AppState } from '../state/store'; import Swal from 'sweetalert2'; import { useRouter } from 'next/router'; +import { useSpeciesDb } from '../components/shared/useSpeciesDb'; +import { useReferenceDb } from '../components/shared/useReferenceDb'; +import OccurrenceCard from '../components/editPointData/OccurrenceCard'; +import InsecticideResistanceCard from '../components/editPointData/InsecticideResistanceCard'; +import BionomicsCard from '../components/editPointData/BionomicsCard'; import AuthWrapper from '../components/shared/AuthWrapper'; import { RolesEnum } from '../state/state.types'; -const ENTITY_OPTIONS = [ - 'Larval_site', - 'ace1AlleleFrequencies', - 'ace1GenotypeFrequencies', - 'ace1MethodAndSample', - 'anthropo_zoophagic', - 'biology', - 'bionomics', - 'biting_activity', - 'biting_rate', - 'cyp4j5AlleleFrequencies', - 'cyp4j5GenotypeFrequencies', - 'cyp6aapAlleleFrequencies', - 'cyp6aapGenotypeFrequencies', - 'cyp6p4AlleleFrequencies', - 'cyp6p4GenotypeFrequencies', - 'cytochromesP450_cypMethodAndSample', - 'dataset', - 'endo_exophagic', - 'endo_exophily', - 'environment', - 'genotypicRepresentativeness', - 'gste2_114AlleleFrequencies', - 'gste2_114GenotypeFrequencies', - 'gste2_119AlleleFrequencies', - 'gste2_119GenotypeFrequencies', - 'gsteMethodAndSample', - 'infection', - 'insecticideResistanceBioassays', - 'kdrGenotypeFrequencies', - 'occurrence', - 'rdl296AlleleFrequencies', - 'rdl296GenotypeFrequencies', - 'rdlMethodAndSample', - 'recorded_species', - 'reference', - 'sample', - 'site', - 'uploaded_dataset', - 'vgsc1570AlleleFrequencies', - 'vgsc1570GenotypeFrequencies', - 'vgsc402AlleleFrequencies', -] as const; - -type EntityType = (typeof ENTITY_OPTIONS)[number]; - -const isPrimitive = (val: unknown): val is string | number | boolean | null => - typeof val === 'string' || - typeof val === 'number' || - typeof val === 'boolean' || - val === null; - -const isBoolean = (val: unknown): val is boolean => typeof val === 'boolean'; +const OCCURRENCE_TABLES = ['occurrence'] as const; +const BIONOMICS_TABLES = ['bionomics'] as const; +const IR_TABLES = ['insecticideResistanceBioassays'] as const; + +const ALL_ENTITIES = [...OCCURRENCE_TABLES, ...BIONOMICS_TABLES, ...IR_TABLES]; +type EntityType = (typeof ALL_ENTITIES)[number]; const EditPointData: React.FC = () => { const [mode, setMode] = useState<'occurrence' | 'source'>('occurrence'); + const [tabIndex, setTabIndex] = useState(0); const [occurrenceId, setOccurrenceId] = useState(''); const [sourceId, setSourceId] = useState(''); - const [entityType, setEntityType] = useState(null); + const [entityType, setEntityType] = useState('occurrence'); + const [data, setData] = useState(null); const [sourceRecords, setSourceRecords] = useState([]); const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); + const t = useTranslations('EditPointData'); + const token = useSelector((state: AppState) => state.auth.token); const [currentUser, setCurrentUser] = useState<{ name?: string; email?: string; }>({}); - const [reasonForEdit, setReasonForEdit] = useState(''); const router = useRouter(); + const speciesList = useSpeciesDb(true); + const referenceList = useReferenceDb(true); + + // Fetch logged-in user for audit trails useEffect(() => { const fetchCurrentUser = async () => { try { const res = await fetch('/api/auth/me', { headers: { Authorization: `Bearer ${token}` }, }); - const data = await res.json(); - setCurrentUser({ name: data.name, email: data.email }); - } catch (err) { - console.error('Failed to fetch current user', err); - } + const userData = await res.json(); + setCurrentUser({ name: userData.name, email: userData.email }); + } catch (err) {} }; - if (token) fetchCurrentUser(); }, [token]); - // 🔹 Load data passed from the Edit button useEffect(() => { const stored = sessionStorage.getItem('editData'); if (stored) { const parsed = JSON.parse(stored); if (parsed.occurrenceId) setOccurrenceId(parsed.occurrenceId); - if (parsed.entityType) setEntityType(parsed.entityType); + + if (parsed.entityType) { + setEntityType(parsed.entityType); + if (OCCURRENCE_TABLES.includes(parsed.entityType)) setTabIndex(0); + else if (BIONOMICS_TABLES.includes(parsed.entityType)) setTabIndex(1); + else if (IR_TABLES.includes(parsed.entityType)) setTabIndex(2); + } + + if (parsed.occurrenceId && parsed.entityType) { + setLoading(true); + getPointData(parsed.entityType, parsed.occurrenceId) + .then((fetched) => setData(fetched)) + .catch(() => toast.error('Failed to auto-fetch data')) + .finally(() => setLoading(false)); + } } }, []); - const fetchDataByOccurrence = async () => { - if (!entityType || !occurrenceId) { - toast.warning('Please enter an ID and select an entity type'); - return; + // Tying the active Tab directly to the Database Query + const handleTabChange = async ( + event: React.SyntheticEvent, + newValue: number + ) => { + setTabIndex(newValue); + setData(null); + + let targetEntity: EntityType = 'occurrence'; + if (newValue === 0) targetEntity = 'occurrence'; + if (newValue === 1) targetEntity = 'bionomics'; + if (newValue === 2) targetEntity = 'insecticideResistanceBioassays'; + + setEntityType(targetEntity); + + if (!occurrenceId) return; + + setLoading(true); + try { + const fetched = await getPointData(targetEntity, occurrenceId); + setData(fetched); + } catch (err) { + toast.error(`Failed to fetch ${targetEntity} data`); + setData(null); + } finally { + setLoading(false); } + }; + + const fetchDataByOccurrence = async () => { + if (!occurrenceId) return toast.warning('Please enter an Occurrence ID'); + + let targetEntity: EntityType = 'occurrence'; + if (tabIndex === 0) targetEntity = 'occurrence'; + if (tabIndex === 1) targetEntity = 'bionomics'; + if (tabIndex === 2) targetEntity = 'insecticideResistanceBioassays'; + + setEntityType(targetEntity); try { setLoading(true); - const fetched = await getPointData(entityType, occurrenceId); + const fetched = await getPointData(targetEntity, occurrenceId); setData(fetched); } catch (err) { - console.error(err); - toast.error('Failed to fetch data'); + toast.error(`Failed to fetch ${targetEntity} data`); } finally { setLoading(false); } }; const fetchDataBySource = async () => { - if (!sourceId) { - toast.warning('Please enter a source ID and select an entity type'); - return; - } - + if (!sourceId) return toast.warning('Please enter a Source ID'); try { setLoading(true); const fetched = await getPointDataBySource(sourceId); - if (Array.isArray(fetched)) { - setSourceRecords(fetched); - } else { - setSourceRecords([fetched]); - } + setSourceRecords(Array.isArray(fetched) ? fetched : [fetched]); } catch (err) { - console.error(err); toast.error('Failed to fetch source records'); } finally { setLoading(false); @@ -170,22 +167,19 @@ const EditPointData: React.FC = () => { setOccurrenceId(record.id?.toString() || ''); setData(record); setMode('occurrence'); + setEntityType('occurrence'); + setTabIndex(0); }; - const handleChange = ( - e: React.ChangeEvent< - HTMLInputElement | HTMLTextAreaElement | { name?: string; value: unknown } - >, - index?: number - ) => { + const handleChange = (e: any, index?: number) => { const { name, value } = e.target; if (!data || !name) return; - // convert values properly let parsedValue: any = value; if (value === 'true') parsedValue = true; else if (value === 'false') parsedValue = false; - else if (!isNaN(Number(value)) && value !== '') parsedValue = Number(value); + else if (!isNaN(Number(value)) && value !== '' && typeof value === 'string') + parsedValue = Number(value); if (Array.isArray(data)) { if (index === undefined) return; @@ -198,58 +192,19 @@ const EditPointData: React.FC = () => { }; const handleSave = async () => { - if (!data || !entityType) { - toast.warning('Missing data or entity type.'); - return; - } - + if (!data || !entityType) + return toast.warning('Missing data or entity type.'); try { const { value: reason } = await Swal.fire({ title: 'Reason for Edit', input: 'textarea', - inputLabel: 'Please enter a reason for editing this record:', inputPlaceholder: 'e.g., Corrected mislabelled coordinates or updated field data', - inputAttributes: { - 'aria-label': 'Reason for editing', - style: - 'min-height: 120px; width: 90%; resize: vertical; font-size: 15px; padding: 10px;', - }, - showCancelButton: true, - confirmButtonText: 'Continue', - cancelButtonText: 'Cancel', - confirmButtonColor: '#28a745', - cancelButtonColor: '#d33', - customClass: { - popup: 'swal2-large-popup', - }, - inputValidator: (value: any) => { - if (!value) { - return 'You must provide a reason before proceeding!'; - } - return null; - }, - }); - - if (!reason) { - return; - } - - setReasonForEdit(reason); - - const confirmResult = await Swal.fire({ - title: 'Check Related Records?', - text: 'There might be other records with the same Source ID that also need updates. Continue saving this one?', - icon: 'warning', showCancelButton: true, - confirmButtonText: 'Yes, continue', - confirmButtonColor: '#28a745', - cancelButtonText: 'Cancel', + inputValidator: (value) => + !value ? 'You must provide a reason before proceeding!' : null, }); - - if (!confirmResult.isConfirmed) { - return; - } + if (!reason) return; setSaving(true); await modifyFullPointData( @@ -258,14 +213,12 @@ const EditPointData: React.FC = () => { currentUser, reason ); - Swal.fire({ icon: 'success', title: 'Saved Successfully', - text: 'The record has been updated successfully.', + text: 'The record has been updated.', }); } catch (err) { - console.error(err); Swal.fire({ icon: 'error', title: 'Error', @@ -276,157 +229,152 @@ const EditPointData: React.FC = () => { } }; - const renderField = (key: string, value: unknown, index?: number) => { - if (key === 'id' || !isPrimitive(value)) return null; - - if ( - isBoolean(value) || - (typeof value === 'string' && - (value.toLowerCase() === 'true' || value.toLowerCase() === 'false')) - ) { - return ( - - {key} - - - ); - } - - return ( - handleChange(e, index)} - /> - ); - }; - - const handleViewLogs = () => { - router.push('/editLogsViewer'); - }; + const hasDataToSave = + data && + (Array.isArray(data) + ? Object.keys(data[0] || {}).length > 0 + : Object.keys(data).length > 0); return (
- + - <> - - - Edit Point Data - - - {/* Toggle between Occurrence or Source */} - + + + + + Edit Point Data + + {loading && } + + + {/* MODE TOGGLES */} + - {/* MODE 1: Occurrence */} + {/* DOMAIN TABS */} + + + + + + + + + {/* OCCURRENCE SEARCH */} {mode === 'occurrence' && ( - <> -

- Source Id:{' '} - {sourceId} -

- + setOccurrenceId(e.target.value)} + sx={{ flex: 1, minWidth: 250, bgcolor: 'white' }} + /> + - - + {loading ? ( + + ) : ( + 'Fetch Data' + )} + +
)} - {/* MODE 2: Source */} + {/* SOURCE SEARCH */} {mode === 'source' && ( - + setSourceId(e.target.value)} - fullWidth - sx={{ flex: 1, minWidth: 250 }} + onChange={(e) => setSourceId(e.target.value)} + sx={{ flex: 1, minWidth: 250, bgcolor: 'white' }} /> - )} - {/* Source record list */} + {/* SOURCE RECORD LIST */} {mode === 'source' && sourceRecords.length > 0 && ( - + Select a Record to Edit - {sourceRecords.map((rec: any, idx: any) => ( + {sourceRecords.map((rec, idx) => ( { mb: 2, }} > - + Record {idx + 1} (ID: {rec.id}) - + {saving ? ( + + ) : ( + 'Save Changes' + )} + )} - - + +
From 415c748911dbfff85425412a49462f6d8d676003 Mon Sep 17 00:00:00 2001 From: K-Owiti Date: Sun, 30 Aug 2026 22:24:56 +0300 Subject: [PATCH 2/2] lint fix --- src/API/src/db/occurrence/occurrence.service.ts | 4 +--- src/UI/components/editPointData/BionomicsCard.tsx | 5 ++--- .../components/editPointData/InsecticideResistanceCard.tsx | 2 +- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/API/src/db/occurrence/occurrence.service.ts b/src/API/src/db/occurrence/occurrence.service.ts index 454db1bc1..cd1e8f863 100644 --- a/src/API/src/db/occurrence/occurrence.service.ts +++ b/src/API/src/db/occurrence/occurrence.service.ts @@ -1254,9 +1254,7 @@ export class OccurrenceService { const isNum = /^\d+$/.test(source_id); // Dynamic OR conditions - const whereConditions: any[] = [ - { source_id: source_id }, - ]; + const whereConditions: any[] = [{ source_id: source_id }]; if (isUuid) { // If it looks like a UUID, also check the true Reference relation ID diff --git a/src/UI/components/editPointData/BionomicsCard.tsx b/src/UI/components/editPointData/BionomicsCard.tsx index 3e461dbad..338e80b00 100644 --- a/src/UI/components/editPointData/BionomicsCard.tsx +++ b/src/UI/components/editPointData/BionomicsCard.tsx @@ -59,7 +59,6 @@ const BionomicsCard: React.FC = ({ data, onChange }) => { ); - const renderBoolean = ( key: string, label: string, @@ -140,9 +139,9 @@ const BionomicsCard: React.FC = ({ data, onChange }) => { {renderBoolean('insecticide_control', 'Insecticide Control Used?')} - {renderBoolean('itn_use', 'ITN Use?', true)} + {renderBoolean('itn_use', 'ITN Use?', true)} {renderField('control', 'Control Details')} - + = ({ ); } - // Helper to render standard text/number fields + // Helper to render standard text/number fields const renderField = (key: string, label: string, type: string = 'text') => (