QTSC7SK437F5SFMYSP74I6WRWU2KSKYQJNPCPSSXWRMIFZZPQ6WQC LD5U5NHLCULPSPQYYMHGHY3HJ3H5PPLZU4O4UALILXPLNAFVBMMQC UCDTBEK3CF6YT2H6V57HI6FAFW44BIYYAK3Z2QJ5LJE7QWX7OEYAC EUEH65HBT4XZXCNWECJXNDEQAWR2NLNSAXFPXLMQ27NOVMQBJT5QC ROQGXQWL2V363K3W7TVVYKIAX4N4IWRERN5BJ7NYJRRVB6OMIJ4QC YX7LU4WRAUDMWS3DEDXZDSF6DXBHLYDWVSMSRK6KIW3MO6GRXSVQC M3JUJ2WWZGCVMBITKRM5FUJMHFYL2QRMXJUVRUE4AC2RF74AOL5AC 2WKGHT2TVFMQT7VUL5OCYVNE365QOYNRXA34LOZ4DBFJ2ZVB4XQQC 4FBIL6IZUDNCXTM6EUHTEOJRHVI4LIIX4BU2IXPXKR362GKIAJMQC DOQBQX4IQSDBYYSBP4BEMTMJUKZPSC33KKXPAGZ3A5BRJMMKHCRQC HM75N4NTZ4BBSSDC7TUSYOQ4SIF3G6KPZA5QRYCVCVRSKQVTJAXAC 2OYSY7VNMQ4C3CQMX7VKKD2JSZM72TJ3LL4GEDUQYG6RJTNIROLAC ZYT3JRERMYXLMJHLPZYQHAINVMPQLBKGGN7A4C7OTVZDY42ZLTKQC * Protected API route to search eBird taxonomy** @route GET /api/ebird/search* @authentication Required* @param {string} q - Search query (common name, scientific name, family, or species code)* @returns {Object} Response containing:* - data: Array of matching eBird taxonomy entries* @description Searches the eBird taxonomy v2024 materialized view for species matching the query.* Searches across primary_com_name, sci_name, family, and species_code fields.*/app.get("/api/ebird/search", authenticate, async (c) => {try {const query = c.req.query("q");if (!query || query.trim().length === 0) {return c.json({error: "Missing or empty query parameter 'q'"}, 400);}if (query.length < 2) {return c.json({error: "Query must be at least 2 characters long"}, 400);}// Database connectionconst sql = neon(c.env.DATABASE_URL);const db = drizzle(sql);// Search across multiple fields with case-insensitive partial matchingconst searchTerm = `%${query.trim().toLowerCase()}%`;// Use raw SQL query for materialized view until Drizzle typing issue is resolvedconst results = await db.execute(sqlExpr`SELECTid,species_code as "speciesCode",primary_com_name as "primaryComName",sci_name as "sciName",bird_order as "birdOrder",familyFROM ebird_taxonomy_v2024WHERELOWER(primary_com_name) LIKE ${searchTerm} ORLOWER(sci_name) LIKE ${searchTerm} ORLOWER(family) LIKE ${searchTerm} ORLOWER(species_code) LIKE ${searchTerm}ORDER BY primary_com_nameLIMIT 20`);return c.json({data: results.rows || results});} catch (error) {console.error("Error searching eBird taxonomy:", error);return c.json({error: "Failed to search eBird taxonomy",details: error instanceof Error ? error.message : String(error),},500);}});/**
* Protected API route to create a new species** @route POST /api/species* @authentication Required* @param {Object} body - Species data including datasetId, label, description, ebirdCode, callTypes* @returns {Object} Response containing:* - data: Created species object with call types* @description Creates a new species with optional eBird integration and call types.* Requires EDIT permission on the dataset. Creates entries in species, species_dataset,* and optionally call_type tables in a single transaction.*/app.post("/api/species", authenticate, async (c) => {try {// Get user ID from JWTconst jwtPayload = (c as unknown as { jwtPayload: JWTPayload }).jwtPayload;const userId = jwtPayload.sub;// Database connectionconst sql = neon(c.env.DATABASE_URL);const db = drizzle(sql);// Parse and validate request bodyconst body = await c.req.json();const { id, datasetId, label, description, ebirdCode, callTypes } = body;// Field validationif (!id || typeof id !== 'string') {return c.json({error: "Missing or invalid required field: id"}, 400);}if (!datasetId || typeof datasetId !== 'string') {return c.json({error: "Missing or invalid required field: datasetId"}, 400);}if (!label || typeof label !== 'string' || label.trim().length === 0) {return c.json({error: "Missing or invalid required field: label"}, 400);}// Length validationif (id.length !== 12) {return c.json({error: "Field 'id' must be exactly 12 characters (nanoid)"}, 400);}if (label.length > 100) {return c.json({error: "Field 'label' must be 100 characters or less"}, 400);}if (description && description.length > 255) {return c.json({error: "Field 'description' must be 255 characters or less"}, 400);}if (ebirdCode && ebirdCode.length > 12) {return c.json({error: "Field 'ebirdCode' must be 12 characters or less"}, 400);}// Validate call types if providedif (callTypes && (!Array.isArray(callTypes) || callTypes.some(ct => !ct.label || ct.label.length > 100))) {return c.json({error: "Field 'callTypes' must be an array of objects with 'label' field (max 100 chars)"}, 400);}// Check if user has EDIT permission on the datasetconst hasPermission = await checkUserPermission(db, userId, datasetId.trim(), 'EDIT');if (!hasPermission) {return c.json({error: "You don't have permission to create species in this dataset"}, 403);}// Start transaction for atomic creationconst result = await db.transaction(async (tx) => {const now = new Date();// Create species recordconst newSpecies = {id: id.trim(),label: label.trim(),ebirdCode: ebirdCode?.trim() || null,taxonomyVersion: ebirdCode ? '2024' : null,description: description?.trim() || null,createdBy: userId,createdAt: now,lastModified: now,modifiedBy: userId,owner: userId,active: true,};const [createdSpecies] = await tx.insert(species).values(newSpecies).returning({id: species.id,label: species.label,ebirdCode: species.ebirdCode,taxonomyVersion: species.taxonomyVersion,description: species.description,createdAt: species.createdAt,createdBy: species.createdBy,lastModified: species.lastModified,modifiedBy: species.modifiedBy,active: species.active,});// Create species-dataset associationawait tx.insert(speciesDataset).values({speciesId: id.trim(),datasetId: datasetId.trim(),createdAt: now,createdBy: userId,lastModified: now,modifiedBy: userId,});// Create call types if providedconst createdCallTypes = [];if (callTypes && callTypes.length > 0) {for (const callTypeData of callTypes) {const callTypeId = nanoid(12);const [createdCallType] = await tx.insert(callType).values({id: callTypeId,speciesId: id.trim(),label: callTypeData.label.trim(),createdBy: userId,createdAt: now,lastModified: now,modifiedBy: userId,active: true,}).returning({id: callType.id,label: callType.label,});createdCallTypes.push(createdCallType);}}return {...createdSpecies,callTypes: createdCallTypes};});console.log("Created species:", result.id, "for dataset:", datasetId, "by user:", userId);return c.json({data: result}, 201);} catch (error) {console.error("Error creating species:", error);// Handle unique constraint violationsif (error instanceof Error && error.message.includes('duplicate key')) {return c.json({error: "A species with this ID already exists"}, 400);}return c.json({error: "Failed to create species",details: error instanceof Error ? error.message : String(error),},500);}});/*** Protected API route to update an existing species** @route PUT /api/species/:id* @authentication Required* @param {string} id - Species ID in URL path* @param {Object} body - Species data to update (label, description, ebirdCode, active)* @returns {Object} Response containing:* - data: Updated species object with call types* @description Updates an existing species. Requires EDIT permission on the dataset.* When soft deleting (active=false), also soft deletes all related call types.*/app.put("/api/species/:id", authenticate, async (c) => {try {// Get user from JWTconst jwtPayload = (c as unknown as { jwtPayload: JWTPayload }).jwtPayload;const userId = jwtPayload.sub;// Get species ID from URL parametersconst speciesId = c.req.param("id");// Parse request bodyconst body = await c.req.json();const { label, description, ebirdCode, active } = body;// Connect to databaseconst sql = neon(c.env.DATABASE_URL);const db = drizzle(sql);// Check if species exists and get its associated datasetsconst existingSpecies = await db.select({id: species.id,active: species.active}).from(species).where(eq(species.id, speciesId)).limit(1);if (existingSpecies.length === 0) {return c.json({error: "Species not found"}, 404);}// Get datasets associated with this species to check permissionsconst associatedDatasets = await db.select({ datasetId: speciesDataset.datasetId }).from(speciesDataset).where(eq(speciesDataset.speciesId, speciesId));if (associatedDatasets.length === 0) {return c.json({error: "Species not associated with any dataset"}, 400);}// Check if user has EDIT permission on at least one associated datasetlet hasPermission = false;for (const assoc of associatedDatasets) {if (await checkUserPermission(db, userId, assoc.datasetId, 'EDIT')) {hasPermission = true;break;}}if (!hasPermission) {return c.json({error: "You don't have permission to edit this species"}, 403);}// Start transaction for atomic updateconst result = await db.transaction(async (tx) => {// Build update object with only provided fieldsconst updateData: Record<string, unknown> = {lastModified: new Date(),modifiedBy: userId,};// Validate and add label if providedif (label !== undefined) {if (typeof label !== 'string' || label.trim().length === 0) {return c.json({error: "Field 'label' must be a non-empty string"}, 400);}if (label.length > 100) {return c.json({error: "Field 'label' must be 100 characters or less"}, 400);}updateData.label = label.trim();}// Validate and add description if providedif (description !== undefined) {if (description !== null && typeof description !== 'string') {return c.json({error: "Field 'description' must be a string or null"}, 400);}if (description && description.length > 255) {return c.json({error: "Field 'description' must be 255 characters or less"}, 400);}updateData.description = description?.trim() || null;}// Validate and add eBird code if providedif (ebirdCode !== undefined) {if (ebirdCode !== null && typeof ebirdCode !== 'string') {return c.json({error: "Field 'ebirdCode' must be a string or null"}, 400);}if (ebirdCode && ebirdCode.length > 12) {return c.json({error: "Field 'ebirdCode' must be 12 characters or less"}, 400);}updateData.ebirdCode = ebirdCode?.trim() || null;updateData.taxonomyVersion = ebirdCode ? '2024' : null;}// Add active status if provided (for soft delete)if (active !== undefined) {if (typeof active !== 'boolean') {return c.json({error: "Field 'active' must be a boolean"}, 400);}updateData.active = active;// If soft deleting species, also soft delete related recordsif (!active) {// Soft delete all call types for this speciesawait tx.update(callType).set({active: false,lastModified: new Date(),modifiedBy: userId}).where(eq(callType.speciesId, speciesId));// Delete all species-dataset associations for this species (hard delete since no active field)await tx.delete(speciesDataset).where(eq(speciesDataset.speciesId, speciesId));}}// Update the speciesconst [updatedSpecies] = await tx.update(species).set(updateData).where(eq(species.id, speciesId)).returning({id: species.id,label: species.label,ebirdCode: species.ebirdCode,taxonomyVersion: species.taxonomyVersion,description: species.description,createdAt: species.createdAt,createdBy: species.createdBy,lastModified: species.lastModified,modifiedBy: species.modifiedBy,active: species.active,});// Get updated call typesconst callTypes = await tx.select({id: callType.id,label: callType.label,}).from(callType).where(and(eq(callType.speciesId, speciesId), eq(callType.active, true)));return {...updatedSpecies,callTypes};});console.log("Updated species:", speciesId, "by user:", userId);return c.json({data: result});} catch (error) {console.error("Error updating species:", error);return c.json({error: "Failed to update species",details: error instanceof Error ? error.message : String(error),},500);}});/*** Protected API route to create a new call type** @route POST /api/call-types* @authentication Required* @param {Object} body - Call type data including id, speciesId, label* @returns {Object} Response containing:* - data: Created call type object* @description Creates a new call type for a species. Requires EDIT permission on any dataset containing the species.*/app.post("/api/call-types", authenticate, async (c) => {try {// Get user ID from JWTconst jwtPayload = (c as unknown as { jwtPayload: JWTPayload }).jwtPayload;const userId = jwtPayload.sub;// Database connectionconst sql = neon(c.env.DATABASE_URL);const db = drizzle(sql);// Parse and validate request bodyconst body = await c.req.json();const { id, speciesId, label } = body;// Field validationif (!id || typeof id !== 'string' || id.length !== 12) {return c.json({error: "Field 'id' must be exactly 12 characters (nanoid)"}, 400);}if (!speciesId || typeof speciesId !== 'string') {return c.json({error: "Missing or invalid required field: speciesId"}, 400);}if (!label || typeof label !== 'string' || label.trim().length === 0) {return c.json({error: "Missing or invalid required field: label"}, 400);}if (label.length > 100) {return c.json({error: "Field 'label' must be 100 characters or less"}, 400);}// Check if species exists and get its associated datasetsconst associatedDatasets = await db.select({ datasetId: speciesDataset.datasetId }).from(speciesDataset).where(eq(speciesDataset.speciesId, speciesId));if (associatedDatasets.length === 0) {return c.json({error: "Species not found or not associated with any dataset"}, 404);}// Check if user has EDIT permission on at least one associated datasetlet hasPermission = false;for (const assoc of associatedDatasets) {if (await checkUserPermission(db, userId, assoc.datasetId, 'EDIT')) {hasPermission = true;break;}}if (!hasPermission) {return c.json({error: "You don't have permission to create call types for this species"}, 403);}// Create call typeconst now = new Date();const [createdCallType] = await db.insert(callType).values({id: id.trim(),speciesId: speciesId.trim(),label: label.trim(),createdBy: userId,createdAt: now,lastModified: now,modifiedBy: userId,active: true,}).returning({id: callType.id,speciesId: callType.speciesId,label: callType.label,createdAt: callType.createdAt,active: callType.active,});console.log("Created call type:", createdCallType.id, "for species:", speciesId, "by user:", userId);return c.json({data: createdCallType}, 201);} catch (error) {console.error("Error creating call type:", error);if (error instanceof Error && error.message.includes('duplicate key')) {return c.json({error: "A call type with this ID already exists"}, 400);}return c.json({error: "Failed to create call type",details: error instanceof Error ? error.message : String(error),},500);}});/*** Protected API route to update an existing call type** @route PUT /api/call-types/:id* @authentication Required* @param {string} id - Call type ID in URL path* @param {Object} body - Call type data to update (label, active)* @returns {Object} Response containing:* - data: Updated call type object* @description Updates an existing call type. Requires EDIT permission on any dataset containing the parent species.*/app.put("/api/call-types/:id", authenticate, async (c) => {try {// Get user from JWTconst jwtPayload = (c as unknown as { jwtPayload: JWTPayload }).jwtPayload;const userId = jwtPayload.sub;// Get call type ID from URL parametersconst callTypeId = c.req.param("id");// Parse request bodyconst body = await c.req.json();const { label, active } = body;// Connect to databaseconst sql = neon(c.env.DATABASE_URL);const db = drizzle(sql);// Check if call type exists and get its speciesconst existingCallType = await db.select({id: callType.id,speciesId: callType.speciesId,active: callType.active}).from(callType).where(eq(callType.id, callTypeId)).limit(1);if (existingCallType.length === 0) {return c.json({error: "Call type not found"}, 404);}const speciesId = existingCallType[0].speciesId;// Get datasets associated with the species to check permissionsconst associatedDatasets = await db.select({ datasetId: speciesDataset.datasetId }).from(speciesDataset).where(eq(speciesDataset.speciesId, speciesId));// Check if user has EDIT permission on at least one associated datasetlet hasPermission = false;for (const assoc of associatedDatasets) {if (await checkUserPermission(db, userId, assoc.datasetId, 'EDIT')) {hasPermission = true;break;}}if (!hasPermission) {return c.json({error: "You don't have permission to edit this call type"}, 403);}// Build update object with only provided fieldsconst updateData: Record<string, unknown> = {lastModified: new Date(),modifiedBy: userId,};// Validate and add label if providedif (label !== undefined) {if (typeof label !== 'string' || label.trim().length === 0) {return c.json({error: "Field 'label' must be a non-empty string"}, 400);}if (label.length > 100) {return c.json({error: "Field 'label' must be 100 characters or less"}, 400);}updateData.label = label.trim();}// Add active status if provided (for soft delete)if (active !== undefined) {if (typeof active !== 'boolean') {return c.json({error: "Field 'active' must be a boolean"}, 400);}updateData.active = active;}// Update the call typeconst [updatedCallType] = await db.update(callType).set(updateData).where(eq(callType.id, callTypeId)).returning({id: callType.id,speciesId: callType.speciesId,label: callType.label,createdAt: callType.createdAt,lastModified: callType.lastModified,active: callType.active,});console.log("Updated call type:", callTypeId, "by user:", userId);return c.json({data: updatedCallType});} catch (error) {console.error("Error updating call type:", error);return c.json({error: "Failed to update call type",details: error instanceof Error ? error.message : String(error),},500);}});/**
const [showForm, setShowForm] = useState<boolean>(false);const [formLoading, setFormLoading] = useState<boolean>(false);const [formError, setFormError] = useState<string | null>(null);const [editingSpecies, setEditingSpecies] = useState<Species | null>(null);const [isEditMode, setIsEditMode] = useState<boolean>(false);// Form stateconst [formData, setFormData] = useState({label: '',description: '',ebirdCode: '',ebirdData: null as EbirdSearchResult | null});// Call types stateconst [callTypes, setCallTypes] = useState<Array<{id: string, label: string}>>([]);
const handleSubmitSpecies = async (e: React.FormEvent) => {e.preventDefault();if (!isAuthenticated) return;setFormLoading(true);setFormError(null);try {const accessToken = await getAccessToken();if (isEditMode && editingSpecies) {// Update existing speciesconst response = await fetch(`/api/species/${editingSpecies.id}`, {method: "PUT",headers: {"Authorization": `Bearer ${accessToken}`,"Content-Type": "application/json",},body: JSON.stringify({label: formData.label,description: formData.description || null,ebirdCode: formData.ebirdCode || null,}),});if (!response.ok) {const errorData = await response.json();throw new Error(errorData.error || `HTTP error! Status: ${response.status}`);}const { data: updatedSpecies } = await response.json();// Update the species in the listsetSpecies(prev => prev.map(s =>s.id === updatedSpecies.id ? updatedSpecies : s));} else {// Create new speciesconst id = nanoid(12);const response = await fetch("/api/species", {method: "POST",headers: {"Authorization": `Bearer ${accessToken}`,"Content-Type": "application/json",},body: JSON.stringify({id,datasetId,label: formData.label,description: formData.description || null,ebirdCode: formData.ebirdCode || null,callTypes: callTypes.filter(ct => ct.label.trim()).map(ct => ({ label: ct.label })),}),});if (!response.ok) {const errorData = await response.json();throw new Error(errorData.error || `HTTP error! Status: ${response.status}`);}const { data: newSpecies } = await response.json();// Add the new species to the listsetSpecies(prev => [newSpecies, ...prev]);}// Reset form and close modalhandleCancelForm();} catch (err) {setFormError(err instanceof Error ? err.message : `Failed to ${isEditMode ? 'update' : 'add'} species`);console.error(`Error ${isEditMode ? 'updating' : 'adding'} species:`, err);} finally {setFormLoading(false);}};const handleFormChange = (field: keyof typeof formData, value: string | EbirdSearchResult | null) => {setFormData(prev => ({...prev,[field]: value}));};const handleCancelForm = () => {setShowForm(false);setFormError(null);setIsEditMode(false);setEditingSpecies(null);setFormData({label: '',description: '',ebirdCode: '',ebirdData: null});setCallTypes([]);};const handleCreateNew = () => {setIsEditMode(false);setEditingSpecies(null);setFormData({label: '',description: '',ebirdCode: '',ebirdData: null});setCallTypes([]);setShowForm(true);};const handleEditSpecies = (speciesItem: Species) => {setIsEditMode(true);setEditingSpecies(speciesItem);// If species has an eBird code, create a mock eBird data object for displayconst ebirdData = speciesItem.ebirdCode ? {id: '',speciesCode: speciesItem.ebirdCode,primaryComName: speciesItem.label,sciName: '',birdOrder: null,family: null} : null;setFormData({label: speciesItem.label,description: speciesItem.description || '',ebirdCode: speciesItem.ebirdCode || '',ebirdData: ebirdData});setCallTypes(speciesItem.callTypes || []);setShowForm(true);};
const handleDeleteSpecies = async (speciesItem: Species) => {if (!isAuthenticated) return;const confirmDelete = window.confirm(`Are you sure you want to delete "${speciesItem.label}"? This will also delete all associated call types. This action cannot be undone.`);if (!confirmDelete) return;try {const accessToken = await getAccessToken();const response = await fetch(`/api/species/${speciesItem.id}`, {method: "PUT",headers: {"Authorization": `Bearer ${accessToken}`,"Content-Type": "application/json",},body: JSON.stringify({active: false}),});if (!response.ok) {const errorData = await response.json();throw new Error(errorData.error || `HTTP error! Status: ${response.status}`);}// Remove the species from the list (soft delete)setSpecies(prev => prev.filter(s => s.id !== speciesItem.id));} catch (err) {setError(err instanceof Error ? err.message : "Failed to delete species");console.error("Error deleting species:", err);}};const handleEbirdSelect = (ebirdSpecies: EbirdSearchResult) => {handleFormChange('ebirdData', ebirdSpecies);handleFormChange('ebirdCode', ebirdSpecies.speciesCode);// Auto-fill label if emptyif (!formData.label) {handleFormChange('label', ebirdSpecies.primaryComName);}};const addCallType = () => {setCallTypes(prev => [...prev, { id: nanoid(12), label: '' }]);};const updateCallType = (index: number, label: string) => {setCallTypes(prev => prev.map((ct, i) => i === index ? { ...ct, label } : ct));};const removeCallType = (index: number) => {setCallTypes(prev => prev.filter((_, i) => i !== index));};
<div className="card p-6 bg-white shadow-sm rounded-lg">
<>{/* Header with Add Button */}{isAuthenticated && !authLoading && canCreateSpecies && (<div className="flex justify-between items-center mb-4"><h2 className="text-lg font-semibold">Species</h2><ButtononClick={handleCreateNew}variant="default"size="sm"className="flex items-center gap-2"><Plus className="h-4 w-4" />Add Species</Button></div>)}<div className="card p-6 bg-white shadow-sm rounded-lg">
<TableCell className="font-medium whitespace-normal break-words">{speciesItem.label}</TableCell><TableCell className="whitespace-normal break-words">{speciesItem.ebirdCode || "—"}</TableCell><TableCell className="whitespace-normal break-words">{speciesItem.description || "—"}</TableCell><TableCell className="whitespace-normal break-words">
<TableCellclassName={`font-medium whitespace-normal break-words ${onSpeciesSelect ? 'cursor-pointer' : ''}`}onClick={() => handleSpeciesClick(speciesItem)}>{speciesItem.label}</TableCell><TableCellclassName={`whitespace-normal break-words ${onSpeciesSelect ? 'cursor-pointer' : ''}`}onClick={() => handleSpeciesClick(speciesItem)}>{speciesItem.ebirdCode || "—"}</TableCell><TableCellclassName={`whitespace-normal break-words ${onSpeciesSelect ? 'cursor-pointer' : ''}`}onClick={() => handleSpeciesClick(speciesItem)}>{speciesItem.description || "—"}</TableCell><TableCellclassName={`whitespace-normal break-words ${onSpeciesSelect ? 'cursor-pointer' : ''}`}onClick={() => handleSpeciesClick(speciesItem)}>
{canCreateSpecies && (<TableCell className="text-center"><div className="flex justify-center gap-2"><ButtononClick={(e) => {e.stopPropagation();handleEditSpecies(speciesItem);}}variant="ghost"size="sm"className="p-1 h-8 w-8"title="Edit species"><Edit className="h-4 w-4" /></Button><ButtononClick={(e) => {e.stopPropagation();handleDeleteSpecies(speciesItem);}}variant="ghost"size="sm"className="p-1 h-8 w-8 text-red-600 hover:text-red-700 hover:bg-red-50"title="Delete species"><Trash2 className="h-4 w-4" /></Button></div></TableCell>)}
</div>
</div>{/* Create/Edit Species Modal */}{showForm && (<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"><div className="bg-white rounded-lg p-6 w-full max-w-2xl mx-4 max-h-[90vh] overflow-y-auto"><div className="flex justify-between items-center mb-4"><h3 className="text-lg font-semibold">{isEditMode ? 'Edit Species' : 'Add New Species'}</h3><ButtononClick={handleCancelForm}variant="ghost"size="sm"className="p-1"><X className="h-4 w-4" /></Button></div>{formError && (<div className="bg-red-50 border border-red-200 text-red-700 px-3 py-2 rounded mb-4">{formError}</div>)}<form onSubmit={handleSubmitSpecies} className="space-y-6">{/* Basic Information */}<div className="space-y-4"><h4 className="font-medium text-gray-900">Basic Information</h4><div><label htmlFor="label" className="block text-sm font-medium text-gray-700 mb-1">Species Label *</label><inputtype="text"id="label"requiredmaxLength={100}value={formData.label}onChange={(e) => handleFormChange('label', e.target.value)}className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"placeholder="e.g. Brown Kiwi, Green Gecko, Bell Frog"/></div><div><label htmlFor="description" className="block text-sm font-medium text-gray-700 mb-1">Description</label><textareaid="description"maxLength={255}value={formData.description}onChange={(e) => handleFormChange('description', e.target.value)}className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"placeholder="Optional description"rows={3}/></div></div>{/* eBird Integration */}<div className="space-y-4"><h4 className="font-medium text-gray-900">eBird Integration (Optional)</h4>{formData.ebirdData ? (<div className="p-3 bg-gray-50 border border-gray-200 rounded-md"><div className="flex justify-between items-start"><div><div className="font-medium text-gray-800">{formData.ebirdData.primaryComName}</div>{formData.ebirdData.sciName && (<div className="text-sm text-gray-600 italic">{formData.ebirdData.sciName}</div>)}<div className="text-sm text-gray-600">Code: {formData.ebirdData.speciesCode}</div>{isEditMode && (<div className="text-xs text-gray-500 mt-1">Current eBird selection</div>)}</div><Buttontype="button"onClick={() => {handleFormChange('ebirdData', null);handleFormChange('ebirdCode', '');}}variant="ghost"size="sm"className="p-1 text-gray-600 hover:bg-gray-100"title="Clear eBird selection"><X className="h-4 w-4" /></Button></div></div>) : (<div><p className="text-sm text-gray-600 mb-3">Search for bird species in the eBird taxonomy. Leave empty for non-bird species.</p><label className="block text-sm font-medium text-gray-700 mb-1">Search eBird Species</label><EbirdSearchonSelectSpecies={handleEbirdSelect}placeholder="Search by common name, scientific name, or family..."/></div>)}</div>{/* Call Types */}{!isEditMode && (<div className="space-y-4"><div className="flex justify-between items-center"><h4 className="font-medium text-gray-900">Call Types (Optional)</h4><Buttontype="button"onClick={addCallType}variant="outline"size="sm"className="flex items-center gap-1"><Plus className="h-3 w-3" />Add Call Type</Button></div><p className="text-sm text-gray-600">Examples: male, female, duet, trill, hunting, begging</p>{callTypes.map((callType, index) => (<div key={callType.id} className="flex gap-2"><inputtype="text"value={callType.label}onChange={(e) => updateCallType(index, e.target.value)}className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"placeholder="Call type label"maxLength={100}/><Buttontype="button"onClick={() => removeCallType(index)}variant="ghost"size="sm"className="p-2 text-red-600 hover:bg-red-50"><Trash2 className="h-4 w-4" /></Button></div>))}</div>)}<div className="flex justify-end space-x-3 pt-4 border-t"><Buttontype="button"onClick={handleCancelForm}variant="outline"disabled={formLoading}>Cancel</Button><Buttontype="submit"disabled={formLoading || !formData.label.trim()}>{formLoading? (isEditMode ? "Updating..." : "Adding..."): (isEditMode ? "Update Species" : "Add Species")}</Button></div></form></div></div>)}</>
// Drizzle handles materialized views differently, excluded the materialized view `ebird_taxonomy_v2024` from this conversion. Implement that at the query level in application code.
// Materialized view `ebird_taxonomy_v2024` is now included and can be queried using Drizzle.
import { pgEnum, pgTable, varchar, boolean, timestamp, date, decimal, integer, json, primaryKey, index, unique, foreignKey, uniqueIndex, bigint } from "drizzle-orm/pg-core";
import { pgEnum, pgTable, pgMaterializedView, varchar, boolean, timestamp, date, decimal, integer, json, primaryKey, index, unique, foreignKey, uniqueIndex, bigint } from "drizzle-orm/pg-core";
// eBird Taxonomy Materialized View (2024 version)// This materialized view provides access to the 2024 eBird taxonomy dataexport const ebirdTaxonomyV2024 = pgMaterializedView("ebird_taxonomy_v2024", {id: varchar("id", { length: 12 }).primaryKey(),speciesCode: varchar("species_code", { length: 15 }),primaryComName: varchar("primary_com_name", { length: 100 }),sciName: varchar("sci_name", { length: 100 }),birdOrder: varchar("bird_order", { length: 30 }),family: varchar("family", { length: 100 }),});