LYPSC7BOH6T45FCPRHSCXILAJSJ74D5WSQTUIKPWD5ECXOYGUY5AC 7ESBJZLIH3TAERLH2HIZAAQHVNVHQWLWCOBLMJRFTIL33YITJNIAC HBM7XFBGVMKW3P3VZFNQTJMINZ4DC3D4TZMT4TPTJRXC62HKZMMQC YX7LU4WRAUDMWS3DEDXZDSF6DXBHLYDWVSMSRK6KIW3MO6GRXSVQC J2RLNDEXTGAV4BB6ANIIR7XJLJBHSB4NFQWSBWHNAFB6DMLGS5RAC ROQGXQWL2V363K3W7TVVYKIAX4N4IWRERN5BJ7NYJRRVB6OMIJ4QC 4RBE543WLHA7PIYT4W7YEJPF6XKZ2UGKPJBQ3CTLJ44AOMGHCEYQC POIBWSL3JFHT2KN3STFSJX3INSYKEJTX6KSW3N7BVEKWX2GJ6T7QC M3JUJ2WWZGCVMBITKRM5FUJMHFYL2QRMXJUVRUE4AC2RF74AOL5AC DOQBQX4IQSDBYYSBP4BEMTMJUKZPSC33KKXPAGZ3A5BRJMMKHCRQC 4M3EBLTLSS2BRCM42ZP7WVD4YMRRLGV2P2XF47IAV5XHHJD52HTQC const countResult = await db.select({count: sqlExpr<number>`COUNT(*)`}).from(file).where(whereConditions);
// If filtering by species, we need a more complex query that joins with selections and labelslet countResult;if (speciesId) {// Count only files that have at least one selection labeled with the specified speciescountResult = await db.select({count: sqlExpr<number>`COUNT(DISTINCT ${file.id})`}).from(file).innerJoin(selection, eq(selection.fileId, file.id)).innerJoin(label, eq(label.selectionId, selection.id)).where(sqlExpr`${whereConditions} AND ${label.speciesId} = ${speciesId} AND ${label.active} = true`);} else {// Standard count without species filtercountResult = await db.select({count: sqlExpr<number>`COUNT(*)`}).from(file).where(whereConditions);}
const filesResult = await db.select({id: file.id,fileName: file.fileName,path: file.path,timestampLocal: file.timestampLocal,duration: file.duration,sampleRate: file.sampleRate,locationId: file.locationId,description: file.description,maybeSolarNight: file.maybeSolarNight,maybeCivilNight: file.maybeCivilNight,moonPhase: file.moonPhase,}).from(file).where(whereConditions).orderBy(file.timestampLocal).limit(limitedPageSize).offset(offset);
let filesResult;if (speciesId) {// Get only files that have at least one selection labeled with the specified speciesfilesResult = await db.select({id: file.id,fileName: file.fileName,path: file.path,timestampLocal: file.timestampLocal,duration: file.duration,sampleRate: file.sampleRate,locationId: file.locationId,description: file.description,maybeSolarNight: file.maybeSolarNight,maybeCivilNight: file.maybeCivilNight,moonPhase: file.moonPhase,}).from(file).innerJoin(selection, eq(selection.fileId, file.id)).innerJoin(label, eq(label.selectionId, selection.id)).where(sqlExpr`${whereConditions} AND ${label.speciesId} = ${speciesId} AND ${label.active} = true`).orderBy(file.timestampLocal).groupBy(file.id, file.fileName, file.path, file.timestampLocal, file.duration,file.sampleRate, file.locationId, file.description, file.maybeSolarNight,file.maybeCivilNight, file.moonPhase).limit(limitedPageSize).offset(offset);} else {// Standard query without species filterfilesResult = await db.select({id: file.id,fileName: file.fileName,path: file.path,timestampLocal: file.timestampLocal,duration: file.duration,sampleRate: file.sampleRate,locationId: file.locationId,description: file.description,maybeSolarNight: file.maybeSolarNight,maybeCivilNight: file.maybeCivilNight,moonPhase: file.moonPhase,}).from(file).where(whereConditions).orderBy(file.timestampLocal).limit(limitedPageSize).offset(offset);}
// Fetch species data via selections and labels// This query gets all species that have been labeled in any selection within these filesconst speciesQueryCondition = speciesId? sqlExpr`${selection.fileId} IN (${sqlExpr.raw(fileIdsQuoted)}) AND ${label.speciesId} = ${speciesId} AND ${label.active} = true`: sqlExpr`${selection.fileId} IN (${sqlExpr.raw(fileIdsQuoted)}) AND ${label.active} = true`;const speciesResults = await db.select({fileId: selection.fileId,speciesId: species.id,speciesLabel: species.label,ebirdCode: species.ebirdCode,description: species.description}).from(selection).innerJoin(label, eq(label.selectionId, selection.id)).innerJoin(species, eq(species.id, label.speciesId)).where(speciesQueryCondition);
// Create a map of file ID to species data, grouping species by filespeciesMap = speciesResults.reduce((acc, item) => {if (!acc[item.fileId]) {acc[item.fileId] = [];}// Only add the species if it's not already in the array for this file// (we might have multiple selections with the same species in one file)const existingSpecies = acc[item.fileId].find((s: { id: string }) => s.id === item.speciesId);if (!existingSpecies) {acc[item.fileId].push({id: item.speciesId,label: item.speciesLabel,ebirdCode: item.ebirdCode,description: item.description});}return acc;}, {} as Record<string, Array<{id: string;label: string;ebirdCode: string | null;description: string | null;}>>);
{speciesOptions.length > 0 && (<div className="flex items-center"><selectid="speciesFilter"value={currentSpeciesId || ''}onChange={(e) => onSpeciesFilterChange(e.target.value || null)}className="rounded-md border border-gray-300 bg-white py-1 px-3 text-sm shadow-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"><option value="">No Species Filter</option>{speciesOptions.map(species => (<option key={species.id} value={species.id}>{species.label}</option>))}</select></div>)}
const [speciesFilter, setSpeciesFilter] = useState<string | null>(null);const [speciesOptions, setSpeciesOptions] = useState<SpeciesOption[]>([]);const [hasSpecies, setHasSpecies] = useState<boolean>(false);// We track loading state but don't need to display it since we're showing species in the table, not as a filter headerconst [, setLoadingSpecies] = useState<boolean>(true);
// Fetch species for the dataset to populate the species filter dropdownuseEffect(() => {setLoadingSpecies(true);setSpeciesOptions([]);const fetchSpecies = async () => {if (!isAuthenticated || !datasetId) {setLoadingSpecies(false);setSpeciesOptions([]);return;}try {const accessToken = await getAccessToken();const url = `/api/species?datasetId=${encodeURIComponent(datasetId)}`;const response = await fetch(url, {headers: {Authorization: `Bearer ${accessToken}`,},});if (!response.ok) {throw new Error(`Server error: ${response.status}`);}const data = await response.json() as { data: SpeciesOption[] };if (!data.data || !Array.isArray(data.data)) {throw new Error("Invalid response format");}setSpeciesOptions(data.data);setHasSpecies(data.data.length > 0);} catch (err) {console.error("Error fetching species:", err);// Don't show error for species, just hide the filtersetSpeciesOptions([]);} finally {setLoadingSpecies(false);}};if (isAuthenticated && !authLoading) {fetchSpecies();}}, [isAuthenticated, authLoading, getAccessToken, datasetId]);
{/* Removed header section - now handled in AuthorisedContent */}
{/* Filter controls - always show filters when we have data to filter, even if current filters result in no files */}{!loading && !error && (pagination || speciesOptions.length > 0) && (<div className="mb-4"><FilesFilterclusterId={clusterId}currentFilter={nightFilter}onFilterChange={setNightFilter}currentSpeciesId={speciesFilter}onSpeciesFilterChange={handleSpeciesFilterChange}speciesOptions={speciesOptions}totalFiles={pagination?.totalItems}/></div>)}
{files.map((file) => (<TableRow key={file.id}><TableCell className="font-medium whitespace-normal break-words">{file.fileName}</TableCell><TableCell className="whitespace-normal break-words">{formatDuration(Number(file.duration))}</TableCell><TableCell className="whitespace-normal break-words">{formatMoonPhase(file.moonPhase)}</TableCell>{hasMothMetadata && (<>
{files.length > 0 ? (files.map((file) => (<TableRow key={file.id}><TableCell className="font-medium whitespace-normal break-words">{file.fileName}</TableCell>{hasSpecies && (
{file.mothMetadata?.gain || "—"}
{file.species && file.species.length > 0 ? (<div className="flex flex-wrap gap-1">{file.species.map(species => (<divkey={species.id}className="inline-block px-3 py-1 rounded-full text-xs font-medium bg-stone-200 text-stone-800 text-center whitespace-nowrap overflow-hidden text-ellipsis"style={{ minWidth: '80px', maxWidth: '150px' }}title={species.label} // Show full name on hover>{species.label}</div>))}</div>) : "—"}
)}<TableCell className="whitespace-normal break-words">{formatDuration(Number(file.duration))}</TableCell>{hasMothMetadata && (<><TableCell className="whitespace-normal break-words">{file.mothMetadata?.gain || "—"}</TableCell><TableCell className="whitespace-normal break-words">{formatBatteryVoltage(file.mothMetadata?.batteryV)}</TableCell><TableCell className="whitespace-normal break-words">{formatTemperature(file.mothMetadata?.tempC)}</TableCell></>)}<TableCell className="whitespace-normal break-words">{formatMoonPhase(file.moonPhase)}</TableCell>{hasMetadata && (
{formatBatteryVoltage(file.mothMetadata?.batteryV)}</TableCell><TableCell className="whitespace-normal break-words">{formatTemperature(file.mothMetadata?.tempC)}</TableCell></>)}{hasMetadata && (<TableCell className="whitespace-normal break-words">{file.metadata ? ((() => {try {// Parse metadata if it's a stringconst metaObj = typeof file.metadata === 'string'? JSON.parse(file.metadata): file.metadata;// Format as key-value pairsif (typeof metaObj === 'object' && metaObj !== null) {const pairs = Object.entries(metaObj).map(([key, value]) => {// Handle arrays or objects as valueslet displayValue = value;if (Array.isArray(value)) {// Remove quotes and brackets from array stringdisplayValue = value.join(', ').replace(/[[\]"]*/g, '');} else if (typeof value === 'string') {// Remove quotes from string valuesdisplayValue = value.replace(/^"|"$/g, '');}return `${key}: ${displayValue}`;});
{file.metadata ? ((() => {try {// Parse metadata if it's a stringconst metaObj = typeof file.metadata === 'string'? JSON.parse(file.metadata): file.metadata;
return pairs.join(', ');
// Format as key-value pairsif (typeof metaObj === 'object' && metaObj !== null) {const pairs = Object.entries(metaObj).map(([key, value]) => {// Handle arrays or objects as valueslet displayValue = value;if (Array.isArray(value)) {// Remove quotes and brackets from array stringdisplayValue = value.join(', ').replace(/[[\]"]*/g, '');} else if (typeof value === 'string') {// Remove quotes from string valuesdisplayValue = value.replace(/^"|"$/g, '');}return `${key}: ${displayValue}`;});return pairs.join(', ');}return JSON.stringify(metaObj);} catch (e) {console.error("Error formatting metadata:", e);return typeof file.metadata === 'string'? (file.metadata as string).substring(0, 50): "Invalid metadata";
return JSON.stringify(metaObj);} catch (e) {console.error("Error formatting metadata:", e);return typeof file.metadata === 'string'? (file.metadata as string).substring(0, 50): "Invalid metadata";}})()) : "—"}</TableCell>)}
})()) : "—"}</TableCell>)}</TableRow>))) : (<TableRow><TableCellcolSpan={// Calculate total columns based on optional columns3 + // File, Duration, Moon Phase (always present)(hasSpecies ? 1 : 0) +(hasMothMetadata ? 3 : 0) +(hasMetadata ? 1 : 0)}className="text-center py-8"><div className="text-yellow-800 font-medium">No files found matching the current filters</div><div className="text-gray-500 text-sm mt-2">Try adjusting your filters to see more results</div></TableCell>
const [filesFilter, setFilesFilter] = useState<"none" | "solarNight" | "solarDay" | "civilNight" | "civilDay">("none");
// This filter is now handled in the Files componentconst [filesFilter] = useState<"none" | "solarNight" | "solarDay" | "civilNight" | "civilDay">("none");
{/* Filters moved to breadcrumb line */}{selectedClusterId && (<FilesFilterclusterId={selectedClusterId}currentFilter={filesFilter}onFilterChange={(filter) => setFilesFilter(filter)}/>)}
{/* Filters moved to breadcrumb line - no species filter here since we're using it in Files component */}{/* Removing this filter component since we now have it integrated into the Files component */}