Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,7 @@ export class InsecticideResistanceService {
@InjectRepository(InsecticideResistanceBioassays)
private insecticideResistanceRepository: Repository<InsecticideResistanceBioassays>,
) {}
findOneById(id: string): Promise<InsecticideResistanceBioassays> {
return this.insecticideResistanceRepository.findOne({ where: { id: id } });
}
}
89 changes: 39 additions & 50 deletions src/API/src/db/occurrence/occurrence.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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({
Expand Down Expand Up @@ -1278,8 +1246,29 @@ export class OccurrenceService {
}

async getPointDataBySource(source_id: string): Promise<any> {
// 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'],
});

Expand Down
8 changes: 4 additions & 4 deletions src/UI/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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,
Expand Down
203 changes: 203 additions & 0 deletions src/UI/components/editPointData/BionomicsCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
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<BionomicsCardProps> = ({ data, onChange }) => {
if (!data) return null;

if (Object.keys(data).length === 0) {
return (
<Box
sx={{
p: 4,
mt: 3,
border: '2px dashed #e0e0e0',
borderRadius: 2,
textAlign: 'center',
bgcolor: '#fafafa',
}}
>
<Typography variant="h6" color="textSecondary" sx={{ fontWeight: 500 }}>
There is no bionomics data available for the following occurrence.
</Typography>
</Box>
);
}

// Helper for standard text and number fields
const renderField = (key: string, label: string, type: string = 'text') => (
<Grid item xs={12} sm={6} md={4} key={key}>
<TextField
fullWidth
label={label}
name={key}
type={type}
value={data[key] ?? ''}
onChange={onChange}
margin="normal"
variant="outlined"
sx={{ bgcolor: 'white' }}
/>
</Grid>
);

const renderBoolean = (
key: string,
label: string,
disabled: boolean = false
) => (
<Grid item xs={12} sm={6} md={4} key={key}>
<FormControl
fullWidth
margin="normal"
disabled={disabled}
sx={{ bgcolor: disabled ? '#f5f5f5' : 'white' }}
>
<InputLabel id={`${key}-label`}>{label}</InputLabel>
<Select
labelId={`${key}-label`}
name={key}
value={
data[key] !== undefined && data[key] !== null
? String(data[key])
: ''
}
onChange={onChange as any}
label={label}
>
<MenuItem value="true">True</MenuItem>
<MenuItem value="false">False</MenuItem>
</Select>
</FormControl>
</Grid>
);

return (
<Box sx={{ p: 2 }}>
{/* SECTION 1: Sampling & Study Design */}
<Typography
variant="h6"
sx={{ mt: 2, mb: 1, color: '#555', fontWeight: 600 }}
>
Sampling & Study Design
</Typography>
<Divider sx={{ mb: 2 }} />
<Grid container spacing={2}>
{/* 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')}
</Grid>

{/* SECTION 2: Time & Seasonality */}
<Typography
variant="h6"
sx={{ mt: 4, mb: 1, color: '#555', fontWeight: 600 }}
>
Time & Seasonality
</Typography>
<Divider sx={{ mb: 2 }} />
<Grid container spacing={2}>
{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')}
</Grid>

{/* SECTION 3: Interventions & Control */}
<Typography
variant="h6"
sx={{ mt: 4, mb: 1, color: '#555', fontWeight: 600 }}
>
Interventions & Control
</Typography>
<Divider sx={{ mb: 2 }} />
<Grid container spacing={2}>
{renderBoolean('insecticide_control', 'Insecticide Control Used?')}
{renderBoolean('itn_use', 'ITN Use?', true)}
{renderField('control', 'Control Details')}

<Grid item xs={12} sm={6} md={4}>
<FormControl
fullWidth
margin="normal"
disabled={true}
sx={{ bgcolor: '#f5f5f5' }}
>
<InputLabel id="ir-data-type-label">
Insecticide Resistance Data Type
</InputLabel>
<Select
labelId="ir-data-type-label"
name="insecticide_resistance_data"
value={data.insecticide_resistance_data || ''}
onChange={onChange as any}
label="Insecticide Resistance Data Type"
>
<MenuItem value="">
<em>None / Unknown</em>
</MenuItem>
<MenuItem value="phenotypic">Phenotypic</MenuItem>
<MenuItem value="genotypic">Genotypic</MenuItem>
</Select>
</FormControl>
</Grid>
</Grid>

<Box sx={{ mt: 2 }}>
<TextField
fullWidth
multiline
rows={2}
label="Control Notes"
name="control_notes"
value={data.control_notes ?? ''}
onChange={onChange}
variant="outlined"
sx={{ bgcolor: 'white' }}
/>
</Box>

{/* SECTION 4: Data Curation */}
<Typography
variant="h6"
sx={{ mt: 4, mb: 1, color: '#555', fontWeight: 600 }}
>
Data Curation (Admin)
</Typography>
<Divider sx={{ mb: 2 }} />
<Grid container spacing={2}>
{renderField('data_abstracted_by', 'Abstracted By')}
{renderField('data_checked_by', 'Checked By')}
{renderField('final_check_by', 'Final Check By')}
</Grid>
</Box>
);
};

export default BionomicsCard;
Loading
Loading